Skip to content

Commit 86081ba

Browse files
authored
fix chat thread navigation (#71)
* fix chat thread navigation * add listing attribution snippet * address chat review feedback * address chat fallback comments * hide chat unread dot on thread routes
1 parent f2fbc2d commit 86081ba

17 files changed

Lines changed: 556 additions & 184 deletions

File tree

e2e/chat.spec.ts

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { expect, test } from "@playwright/test";
22
import {
33
DONOR_EMAIL,
4+
HOST_EMAIL,
5+
HOST_SECOND_SEEDED_THREAD_ID,
6+
SECOND_SEEDED_THREAD_ID,
47
SEEDED_THREAD_ID,
58
delayChatSendRequests,
69
failChatSendRequests,
@@ -10,6 +13,20 @@ import {
1013
test("chat loads the seeded thread and composer for a signed-in donor", async ({
1114
page,
1215
}) => {
16+
const maxUpdateDepthErrors: string[] = [];
17+
const recordMaxUpdateDepthError = (message: string) => {
18+
if (message.includes("Maximum update depth exceeded")) {
19+
maxUpdateDepthErrors.push(message);
20+
}
21+
};
22+
23+
page.on("console", (message) => {
24+
recordMaxUpdateDepthError(message.text());
25+
});
26+
page.on("pageerror", (error) => {
27+
recordMaxUpdateDepthError(error.message);
28+
});
29+
1330
await signIn(page, {
1431
email: DONOR_EMAIL,
1532
redirectTo: `/chats/${SEEDED_THREAD_ID}`,
@@ -25,6 +42,61 @@ test("chat loads the seeded thread and composer for a signed-in donor", async ({
2542
);
2643
await expect(page.getByTestId("chat-composer")).toBeVisible();
2744
await expect(page.getByTestId("chat-composer-input")).toBeVisible();
45+
await page
46+
.getByTestId("thread-list")
47+
.evaluate((element) => element.setAttribute("data-persist-check", "true"));
48+
49+
await page.getByTestId(`thread-preview-${SECOND_SEEDED_THREAD_ID}`).click();
50+
await expect(page).toHaveURL(
51+
new RegExp(`/chats/${SECOND_SEEDED_THREAD_ID}$`)
52+
);
53+
await expect(page.getByTestId("thread-list")).toHaveAttribute(
54+
"data-persist-check",
55+
"true"
56+
);
57+
await expect(page.getByTestId("chat-message-list")).toContainText(
58+
"Hi Morgan, are banana peels okay if they are chopped up?"
59+
);
60+
await expect(page.getByTestId("chat-message-list")).toContainText(
61+
"Yes please. Chopped scraps break down much faster in this bay."
62+
);
63+
64+
await page.getByTestId(`thread-preview-${SEEDED_THREAD_ID}`).click();
65+
await expect(page).toHaveURL(new RegExp(`/chats/${SEEDED_THREAD_ID}$`));
66+
await expect(page.getByTestId("chat-message-list")).toContainText(
67+
"Yes, absolutely. Small sealed containers are perfect."
68+
);
69+
70+
await page.waitForTimeout(250);
71+
expect(maxUpdateDepthErrors).toEqual([]);
72+
});
73+
74+
test("chat lists multiple seeded threads for a signed-in host", async ({
75+
page,
76+
}) => {
77+
await signIn(page, {
78+
email: HOST_EMAIL,
79+
redirectTo: `/chats/${SEEDED_THREAD_ID}`,
80+
});
81+
82+
await expect(page).toHaveURL(new RegExp(`/chats/${SEEDED_THREAD_ID}$`));
83+
await expect(page.getByTestId("thread-list")).toBeVisible();
84+
await expect(
85+
page.getByTestId(`thread-preview-${HOST_SECOND_SEEDED_THREAD_ID}`)
86+
).toContainText("Morgan");
87+
88+
await page
89+
.getByTestId(`thread-preview-${HOST_SECOND_SEEDED_THREAD_ID}`)
90+
.click();
91+
await expect(page).toHaveURL(
92+
new RegExp(`/chats/${HOST_SECOND_SEEDED_THREAD_ID}$`)
93+
);
94+
await expect(page.getByTestId("chat-message-list")).toContainText(
95+
"Hi Avery, can the cafe take a few buckets from our community garden working bee?"
96+
);
97+
await expect(page.getByTestId("chat-message-list")).toContainText(
98+
"Yes, drop them by after 3 pm and I'll add them to the cafe pickup."
99+
);
28100
});
29101

30102
test("chat send disables the composer while pending and appends the new message", async ({
@@ -40,7 +112,9 @@ test("chat send disables the composer while pending and appends the new message"
40112
const sendButton = page.getByTestId("chat-composer-send");
41113
const message = `Playwright chat message ${Date.now()}`;
42114

43-
await composerInput.fill(message);
115+
await composerInput.click();
116+
await composerInput.pressSequentially(message);
117+
await expect(sendButton).toBeEnabled();
44118

45119
const messageVisible = page
46120
.getByTestId("chat-message-list")
@@ -65,7 +139,9 @@ test("chat send failures preserve the draft and show inline feedback", async ({
65139
const composerInput = page.getByTestId("chat-composer-input");
66140
const failedMessage = `Chat failure draft ${Date.now()}`;
67141

68-
await composerInput.fill(failedMessage);
142+
await composerInput.click();
143+
await composerInput.pressSequentially(failedMessage);
144+
await expect(page.getByTestId("chat-composer-send")).toBeEnabled();
69145
await page.getByTestId("chat-composer-send").click();
70146

71147
await expect(composerInput).toHaveValue(failedMessage);

e2e/helpers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ export const HOST_EMAIL = "demo-host@peels.local";
44
export const DONOR_EMAIL = "demo-donor@peels.local";
55
export const SEEDED_PASSWORD = "peels-demo-password";
66
export const SEEDED_THREAD_ID = "33333333-3333-4333-8333-333333333333";
7+
export const SECOND_SEEDED_THREAD_ID = "77777777-7777-4777-8777-777777777777";
8+
export const HOST_SECOND_SEEDED_THREAD_ID =
9+
"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
710
export const PROFILE_RENDER_TIMEOUT_MS = 15_000;
811

912
export async function signIn(

src/app/(core)/(interact)/(stretched)/chats/[[...threadId]]/loading.tsx

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/app/(core)/(interact)/(stretched)/chats/[[...threadId]]/error.tsx renamed to src/app/(core)/(interact)/(stretched)/chats/[threadId]/error.tsx

File renamed without changes.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"use client";
2+
3+
import { ChatWindowEmptyState } from "@/components/ChatPageClient";
4+
import PeelsLogo from "@/components/PeelsLogo";
5+
import { useTranslations } from "next-intl";
6+
7+
export default function Loading() {
8+
const t = useTranslations();
9+
10+
return (
11+
<ChatWindowEmptyState aria-busy={true}>
12+
<PeelsLogo size={64} color="emptyState" />
13+
<p>{t("Common.loading")}</p>
14+
</ChatWindowEmptyState>
15+
);
16+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { createClient } from "@/utils/supabase/server";
2+
import { ChatConversationClient } from "@/components/ChatPageClient";
3+
import { getSelectedChatThread, isUuid } from "@/features/chat/chatData";
4+
import { redirect } from "next/navigation";
5+
import type { Metadata } from "next";
6+
7+
type ChatThreadPageProps = {
8+
params: Promise<{
9+
threadId: string;
10+
}>;
11+
};
12+
13+
export const metadata: Metadata = {
14+
title: "Chats",
15+
};
16+
17+
export default async function ChatThreadPage({ params }: ChatThreadPageProps) {
18+
const { threadId } = await params;
19+
const referenceNow = new Date().toISOString();
20+
21+
if (!isUuid(threadId)) {
22+
redirect("/chats");
23+
}
24+
25+
const supabase = await createClient();
26+
const {
27+
data: { user },
28+
} = await supabase.auth.getUser();
29+
30+
if (!user) {
31+
redirect(`/sign-in?redirect_to=/chats/${threadId}`);
32+
}
33+
34+
const selectedThread = await getSelectedChatThread(
35+
supabase,
36+
user.id,
37+
threadId
38+
);
39+
40+
if (!selectedThread) {
41+
redirect("/chats");
42+
}
43+
44+
return (
45+
<ChatConversationClient
46+
user={user}
47+
hasThreads={true}
48+
selectedThread={selectedThread}
49+
referenceNow={referenceNow}
50+
/>
51+
);
52+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { createClient } from "@/utils/supabase/server";
2+
import ChatPageClient from "@/components/ChatPageClient";
3+
import { getChatThreads } from "@/features/chat/chatData";
4+
import { currentPathHeaderName } from "@/utils/supabase/authState";
5+
import { normaliseNextPath } from "@/utils/authRedirects";
6+
import { headers } from "next/headers";
7+
import { redirect } from "next/navigation";
8+
import type { ReactNode } from "react";
9+
10+
export default async function ChatsLayout({
11+
children,
12+
}: {
13+
children: ReactNode;
14+
}) {
15+
const supabase = await createClient();
16+
const {
17+
data: { user },
18+
} = await supabase.auth.getUser();
19+
20+
if (!user) {
21+
const currentPath = (await headers()).get(currentPathHeaderName);
22+
const redirectTo = normaliseNextPath(currentPath, "/chats");
23+
redirect(`/sign-in?redirect_to=${encodeURIComponent(redirectTo)}`);
24+
}
25+
26+
const threads = await getChatThreads(supabase, user.id);
27+
28+
return (
29+
<ChatPageClient user={user} initialThreads={threads}>
30+
{children}
31+
</ChatPageClient>
32+
);
33+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { Metadata } from "next";
2+
3+
export const metadata: Metadata = {
4+
title: "Chats",
5+
};
6+
7+
export default function ChatsIndexPage() {
8+
return null;
9+
}

0 commit comments

Comments
 (0)