Skip to content
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { ToggleSidePanelButton } from "@/ui/messages/toggle-side-panel-button";
import { ProgramHelpLinks } from "@/ui/partners/program-help-links";
import { ProgramRewardsPanel } from "@/ui/partners/program-rewards-panel";
import { X } from "@/ui/shared/icons";
import { Button, Grid, useCopyToClipboard } from "@dub/ui";
import { Button, Grid, useCopyToClipboard, useMediaQuery } from "@dub/ui";
import {
Check,
ChevronLeft,
Expand All @@ -36,13 +36,21 @@ import {
} from "@dub/utils";
import { useAction } from "next-safe-action/hooks";
import Link from "next/link";
import { redirect, useParams } from "next/navigation";
import { useState } from "react";
import {
redirect,
useParams,
useRouter,
useSearchParams,
} from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { v4 as uuid } from "uuid";

export function PartnerMessagesProgramPageClient() {
const { programSlug } = useParams() as { programSlug: string };
const router = useRouter();
const searchParams = useSearchParams();
const { width } = useMediaQuery();

const { user } = useUser();
const { partner } = usePartnerProfile();
Expand Down Expand Up @@ -86,17 +94,35 @@ export function PartnerMessagesProgramPageClient() {
},
});

const program = programMessages?.[0]?.program;
const messages = programMessages?.[0]?.messages;
const { setCurrentPanel, targetThreadId } = useMessagesContext();
const isThreadTransitioning =
targetThreadId !== null && targetThreadId !== programSlug;

const activeThread =
!isThreadTransitioning && programMessages?.[0]?.program.slug === programSlug
? programMessages[0]
: undefined;
const program = activeThread?.program;
const messages = activeThread?.messages;

const { executeAsync: sendMessage } = useAction(messageProgramAction, {
onError({ error }) {
toast.error(parseActionError(error, "Failed to send message"));
},
});

const { setCurrentPanel } = useMessagesContext();
const [isRightPanelOpen, setIsRightPanelOpen] = useState(true);
const [isRightPanelOpen, setIsRightPanelOpen] = useState(false);
const shouldAutoFocusComposer = searchParams.get("new") === "1";

useEffect(() => {
if (typeof width !== "number") return;
setIsRightPanelOpen(width >= 1082);
}, [programSlug, width]);

useEffect(() => {
if (!shouldAutoFocusComposer) return;
router.replace(`/messages/${programSlug}`);
}, [programSlug, router, shouldAutoFocusComposer]);

// Redirect if no messages and not enrolled, or messages error
if (
Expand Down Expand Up @@ -194,6 +220,7 @@ export function PartnerMessagesProgramPageClient() {
currentUserType="partner"
currentUserId={partner?.id || ""}
program={program}
autoFocusComposer={shouldAutoFocusComposer}
onSendMessage={async (message) => {
const createdAt = new Date();

Expand Down
26 changes: 18 additions & 8 deletions apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { NavButton } from "@/ui/layout/page-content/nav-button";
import { MessagesContext, MessagesPanel } from "@/ui/messages/messages-context";
import { MessagesList } from "@/ui/messages/messages-list";
import { ProgramSelector } from "@/ui/partners/program-selector";
import { Button, InfoTooltip, useRouterStuff } from "@dub/ui";
import { Button, InfoTooltip } from "@dub/ui";
import { Msgs, Pen2 } from "@dub/ui/icons";
import { useParams, useRouter } from "next/navigation";
import { CSSProperties, ReactNode, useEffect, useState } from "react";
Expand All @@ -14,7 +14,6 @@ export default function MessagesLayout({ children }: { children: ReactNode }) {
const { programSlug } = useParams() as { programSlug?: string };

const router = useRouter();
const { searchParams } = useRouterStuff();

const { programMessages, isLoading, error } = useProgramMessages({
query: { messagesLimit: 1 },
Expand All @@ -23,13 +22,22 @@ export default function MessagesLayout({ children }: { children: ReactNode }) {
const [currentPanel, setCurrentPanel] = useState<MessagesPanel>(
programSlug ? "main" : "index",
);
const [targetThreadId, setTargetThreadId] = useState<string | null>(null);

useEffect(() => {
searchParams.get("new") && setCurrentPanel("main");
}, [searchParams.get("new")]);
setCurrentPanel(programSlug ? "main" : "index");
setTargetThreadId(null);
}, [programSlug]);

return (
<MessagesContext.Provider value={{ currentPanel, setCurrentPanel }}>
<MessagesContext.Provider
value={{
currentPanel,
setCurrentPanel,
targetThreadId,
setTargetThreadId,
}}
>
<div className="@container/page h-[calc(100dvh-var(--page-top-margin)-var(--page-bottom-margin)-1px)] w-full overflow-hidden rounded-t-[inherit] bg-white">
<div
className="@[800px]/page:grid-cols-[min-content_minmax(340px,1fr)] @[800px]/page:translate-x-0 grid h-full translate-x-[calc(var(--current-panel)*-100%)] grid-cols-[100%_100%]"
Expand Down Expand Up @@ -57,9 +65,11 @@ export default function MessagesLayout({ children }: { children: ReactNode }) {
</div>
<ProgramSelector
selectedProgramSlug={programSlug ?? null}
setSelectedProgramSlug={(slug) =>
router.push(`/messages/${slug}`)
}
setSelectedProgramSlug={(slug) => {
setTargetThreadId(slug);
setCurrentPanel("main");
router.push(`/messages/${slug}?new=1`);
}}
trigger={
<Button
type="button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,28 @@ import { PartnerInfoGroup } from "@/ui/partners/partner-info-group";
import { PartnerInfoSection } from "@/ui/partners/partner-info-section";
import { PartnerInfoStats } from "@/ui/partners/partner-info-stats";
import { X } from "@/ui/shared/icons";
import { Button } from "@dub/ui";
import { Button, useMediaQuery } from "@dub/ui";
import { ChevronLeft } from "@dub/ui/icons";
import { OG_AVATAR_URL, cn } from "@dub/utils";
import { useAction } from "next-safe-action/hooks";
import Link from "next/link";
import { redirect, useParams } from "next/navigation";
import { useState } from "react";
import {
usePathname,
redirect,
useParams,
useRouter,
useSearchParams,
} from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { v4 as uuid } from "uuid";

export function ProgramMessagesPartnerPageClient() {
const { id: workspaceId, slug: workspaceSlug } = useWorkspace();
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const { width } = useMediaQuery();

const { partnerId } = useParams() as { partnerId: string };
const { user } = useUser();
Expand Down Expand Up @@ -71,17 +81,40 @@ export function ProgramMessagesPartnerPageClient() {
},
});

const partner = partnerMessages?.[0]?.partner;
const messages = partnerMessages?.[0]?.messages;
const { setCurrentPanel, targetThreadId } = useMessagesContext();
const isThreadTransitioning =
targetThreadId !== null && targetThreadId !== partnerId;

const activeThread =
!isThreadTransitioning && partnerMessages?.[0]?.partner.id === partnerId
? partnerMessages[0]
: undefined;
const partner = activeThread?.partner;
const messages = activeThread?.messages;

const { executeAsync: sendMessage } = useAction(messagePartnerAction, {
onError({ error }) {
toast.error(parseActionError(error, "Failed to send message"));
},
});

const { setCurrentPanel } = useMessagesContext();
const [isRightPanelOpen, setIsRightPanelOpen] = useState(true);
const [isRightPanelOpen, setIsRightPanelOpen] = useState(false);
const shouldAutoFocusComposer = searchParams.get("new") === "1";

useEffect(() => {
if (typeof width !== "number") return;
setIsRightPanelOpen(width >= 960);
}, [partnerId, width]);

useEffect(() => {
if (!shouldAutoFocusComposer) return;

const nextSearchParams = new URLSearchParams(searchParams.toString());
nextSearchParams.delete("new");
const nextSearch = nextSearchParams.toString();

router.replace(nextSearch ? `${pathname}?${nextSearch}` : pathname);
}, [pathname, router, searchParams, shouldAutoFocusComposer]);

if (errorMessages) redirect(`/${workspaceSlug}/program/messages`);

Expand Down Expand Up @@ -144,6 +177,7 @@ export function ProgramMessagesPartnerPageClient() {
currentUserId={user?.id || ""}
program={program}
partner={partner}
autoFocusComposer={shouldAutoFocusComposer}
onSendMessage={async (message) => {
const createdAt = new Date();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { PartnerSelector } from "@/ui/partners/partner-selector";
import { Button, InfoTooltip } from "@dub/ui";
import { Msgs, Pen2 } from "@dub/ui/icons";
import { useParams, useRouter } from "next/navigation";
import { CSSProperties, ReactNode, useState } from "react";
import { CSSProperties, ReactNode, useEffect, useState } from "react";
import { MessagesUpsell } from "./messages-upsell";

export default function MessagesLayout({ children }: { children: ReactNode }) {
Expand All @@ -30,7 +30,6 @@ export default function MessagesLayout({ children }: { children: ReactNode }) {
function CapableLayout({ children }: { children: ReactNode }) {
const { slug: workspaceSlug } = useWorkspace();
const { partnerId } = useParams() as { partnerId?: string };
const { program } = useProgram();

const router = useRouter();

Expand All @@ -41,9 +40,22 @@ function CapableLayout({ children }: { children: ReactNode }) {
const [currentPanel, setCurrentPanel] = useState<MessagesPanel>(
partnerId ? "main" : "index",
);
const [targetThreadId, setTargetThreadId] = useState<string | null>(null);

useEffect(() => {
setCurrentPanel(partnerId ? "main" : "index");
setTargetThreadId(null);
}, [partnerId]);

return (
<MessagesContext.Provider value={{ currentPanel, setCurrentPanel }}>
<MessagesContext.Provider
value={{
currentPanel,
setCurrentPanel,
targetThreadId,
setTargetThreadId,
}}
>
<div className="@container/page h-[calc(100dvh-var(--page-top-margin)-var(--page-bottom-margin)-1px)] w-full overflow-hidden rounded-t-[inherit] bg-white">
<div
className="@[800px]/page:grid-cols-[min-content_minmax(340px,1fr)] @[800px]/page:translate-x-0 grid h-full translate-x-[calc(var(--current-panel)*-100%)] grid-cols-[100%_100%]"
Expand Down Expand Up @@ -71,9 +83,11 @@ function CapableLayout({ children }: { children: ReactNode }) {
</div>
<PartnerSelector
selectedPartnerId={partnerId ?? null}
setSelectedPartnerId={(id) =>
router.push(`/${workspaceSlug}/program/messages/${id}`)
}
setSelectedPartnerId={(id) => {
setTargetThreadId(id);
setCurrentPanel("main");
router.push(`/${workspaceSlug}/program/messages/${id}?new=1`);
}}
trigger={
<Button
type="button"
Expand Down
20 changes: 14 additions & 6 deletions apps/web/ui/layout/sidebar/app-sidebar-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import useProgram from "@/lib/swr/use-program";
import { useProgramReferralsCount } from "@/lib/swr/use-program-referrals-count";
import useWorkspace from "@/lib/swr/use-workspace";
import useWorkspaces from "@/lib/swr/use-workspaces";
import { useRouterStuff } from "@dub/ui";
import { useMediaQuery, useRouterStuff } from "@dub/ui";
import {
Bell,
Brush,
Expand Down Expand Up @@ -497,6 +497,7 @@ export function AppSidebarNav({
}) {
const { slug: paramsSlug } = useParams() as { slug?: string };
const pathname = usePathname();
const { width, isMobile } = useMediaQuery();
const { getQueryString } = useRouterStuff();
const { data: session, status } = useSession();
const { plan, defaultProgramId } = useWorkspace();
Expand Down Expand Up @@ -546,18 +547,25 @@ export function AppSidebarNav({
session?.user?.["defaultWorkspace"];

const currentArea = useMemo(() => {
const focusedProgramPath =
pathname.includes("/program/campaigns/") ||
pathname.includes("/program/messages/") ||
pathname.endsWith("/program/payouts/success");
const isMobileDrawerViewport =
typeof width === "number" ? width < 768 : isMobile;

return pathname.startsWith("/account/settings")
? "userSettings"
: pathname.startsWith(`/${slug}/settings`)
? "workspaceSettings"
: pathname.includes("/program/campaigns/") ||
pathname.includes("/program/messages/") ||
pathname.endsWith("/program/payouts/success")
? null
: focusedProgramPath
? isMobileDrawerViewport
? "program"
: null
: pathname.startsWith(`/${slug}/program`)
? "program"
: "default";
}, [slug, pathname]);
}, [isMobile, pathname, slug, width]);

const { program } = useProgram({
enabled: Boolean(currentArea === "program" && defaultProgramId),
Expand Down
4 changes: 4 additions & 0 deletions apps/web/ui/messages/messages-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ export type MessagesPanel = "index" | "main";
export const MessagesContext = createContext<{
currentPanel: MessagesPanel;
setCurrentPanel: Dispatch<SetStateAction<MessagesPanel>>;
targetThreadId: string | null;
setTargetThreadId: Dispatch<SetStateAction<string | null>>;
}>({
currentPanel: "index",
setCurrentPanel: () => {},
targetThreadId: null,
setTargetThreadId: () => {},
});

export function useMessagesContext() {
Expand Down
7 changes: 5 additions & 2 deletions apps/web/ui/messages/messages-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function MessagesList({
| undefined;
activeId?: string;
}) {
const { setCurrentPanel } = useMessagesContext();
const { setCurrentPanel, setTargetThreadId } = useMessagesContext();

return (
<div className="flex w-full flex-col">
Expand All @@ -43,7 +43,10 @@ export function MessagesList({
<Link
key={group.id}
href={group.href}
onClick={() => setCurrentPanel("main")}
onClick={() => {
setTargetThreadId(group.id);
setCurrentPanel("main");
}}
className={cn(
"border-border-subtle flex w-full items-center gap-2.5 border-b bg-white px-6 py-4",
group.id === activeId ? "bg-bg-subtle" : "hover:bg-bg-muted",
Expand Down
4 changes: 3 additions & 1 deletion apps/web/ui/messages/messages-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function MessagesPanel({
partner,
onSendMessage,
placeholder,
autoFocusComposer,
error,
}: {
messages?: (Message & { delivered?: boolean })[];
Expand All @@ -40,6 +41,7 @@ export function MessagesPanel({
partner?: Pick<PartnerProps, "name">;
onSendMessage: (message: string) => void;
placeholder?: string;
autoFocusComposer?: boolean;
error?: any;
}) {
const { isMobile } = useMediaQuery();
Expand Down Expand Up @@ -286,7 +288,7 @@ export function MessagesPanel({
<MessageInput
placeholder={personalizedPlaceholder}
onSendMessage={sendMessage}
autoFocus={!isMobile}
autoFocus={!isMobile || autoFocusComposer}
/>
</div>
</div>
Expand Down
Loading