-
Notifications
You must be signed in to change notification settings - Fork 1
fix homepage hydration and chat date rendering #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
6cbe576
d8f4acf
e728452
9ec00e4
5489076
4ccbe88
43274cc
12a3b6e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,70 @@ | ||||||||||
| import { expect, test } from "@playwright/test"; | ||||||||||
|
|
||||||||||
| test("homepage hydrates without chat date mismatches", async ({ | ||||||||||
| browser, | ||||||||||
| page, | ||||||||||
| }) => { | ||||||||||
| const { baseURL, extraHTTPHeaders, locale } = test.info().project.use; | ||||||||||
|
|
||||||||||
| if (typeof baseURL !== "string") { | ||||||||||
| throw new Error("Expected Playwright baseURL to be configured"); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const pageErrors: string[] = []; | ||||||||||
| const consoleErrors: string[] = []; | ||||||||||
|
|
||||||||||
| page.on("pageerror", (error) => { | ||||||||||
| pageErrors.push(error.message); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| page.on("console", (message) => { | ||||||||||
| if (message.type() === "error") { | ||||||||||
| consoleErrors.push(message.text()); | ||||||||||
| } | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| await page.goto("/", { waitUntil: "domcontentloaded" }); | ||||||||||
| await expect( | ||||||||||
| page.getByRole("heading", { | ||||||||||
| name: "Find a home for your food scraps, wherever you are", | ||||||||||
| }) | ||||||||||
| ).toBeVisible(); | ||||||||||
|
|
||||||||||
| const serverContext = await browser.newContext({ | ||||||||||
| baseURL, | ||||||||||
| extraHTTPHeaders, | ||||||||||
| javaScriptEnabled: false, | ||||||||||
| locale, | ||||||||||
| }); | ||||||||||
| const serverPage = await serverContext.newPage(); | ||||||||||
| await serverPage.goto("/"); | ||||||||||
| const serverDayLabels = await serverPage | ||||||||||
| .getByTestId("chat-day-label") | ||||||||||
| .allTextContents(); | ||||||||||
| const serverTimestamps = await serverPage | ||||||||||
| .getByTestId("chat-message-timestamp") | ||||||||||
| .allTextContents(); | ||||||||||
| await serverContext.close(); | ||||||||||
|
|
||||||||||
| await page.waitForLoadState("networkidle"); | ||||||||||
| await page.waitForTimeout(2_000); | ||||||||||
|
||||||||||
| await page.waitForTimeout(2_000); | |
| await expect | |
| .poll(async () => page.getByTestId("chat-day-label").allTextContents()) | |
| .toEqual(["Yesterday", "Today"]); |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -14,12 +14,17 @@ import { | |||||||
| markChatThreadRead, | ||||||||
| sendChatMessage, | ||||||||
| } from "@/components/ChatWindow/chatWindowController"; | ||||||||
| import { formatWeekday } from "@/utils/dateUtils"; | ||||||||
| import { DEMO_CHAT_REFERENCE_TIME } from "@/data/demo/threads"; | ||||||||
| import { | ||||||||
| CHAT_RENDER_TIME_ZONE, | ||||||||
| formatWeekday, | ||||||||
| getChatDateKey, | ||||||||
| } from "@/utils/dateUtils"; | ||||||||
|
|
||||||||
| import { styled } from "next-yak"; | ||||||||
| import { useUnreadMessages } from "@/contexts/UnreadMessagesContext"; | ||||||||
| import { useInlineMutation } from "@/hooks/useInlineMutation"; | ||||||||
| import { useTranslations } from "next-intl"; | ||||||||
| import { useLocale, useTranslations } from "next-intl"; | ||||||||
| import type { | ||||||||
| ChatListing, | ||||||||
| ChatMessageRecord, | ||||||||
|
|
@@ -36,9 +41,29 @@ type ChatWindowProps = { | |||||||
| listing: ChatListing | DemoListing; | ||||||||
| existingThread?: ChatThreadRecord | ChatThreadView | null; | ||||||||
| isDemo?: boolean; | ||||||||
| referenceNow?: string; | ||||||||
| }; | ||||||||
|
|
||||||||
| const DIRECTIONS_FOR_DEMO = ["sent", "received"] as const; | ||||||||
| type ChatRenderOptions = { | ||||||||
| locale: string; | ||||||||
| now?: string; | ||||||||
| timeZone: string; | ||||||||
| useRelativeDayLabels: boolean; | ||||||||
| }; | ||||||||
|
|
||||||||
| const defaultChatRenderOptions: ChatRenderOptions = { | ||||||||
| locale: "en", | ||||||||
| now: undefined, | ||||||||
| timeZone: CHAT_RENDER_TIME_ZONE, | ||||||||
| useRelativeDayLabels: false, | ||||||||
| }; | ||||||||
|
Comment on lines
+65
to
+70
|
||||||||
|
|
||||||||
| function getClientTimeZone() { | ||||||||
| return ( | ||||||||
| Intl.DateTimeFormat().resolvedOptions().timeZone ?? CHAT_RENDER_TIME_ZONE | ||||||||
| ); | ||||||||
| } | ||||||||
|
|
||||||||
| const StyledChatWindow = styled.div` | ||||||||
| height: 100%; | ||||||||
|
|
@@ -115,8 +140,10 @@ const ChatWindow = memo(function ChatWindow({ | |||||||
| listing, | ||||||||
| existingThread = null, | ||||||||
| isDemo = false, | ||||||||
| referenceNow, | ||||||||
| }: ChatWindowProps) { | ||||||||
| const t = useTranslations(); | ||||||||
| const locale = useLocale(); | ||||||||
| const supabase = useMemo(() => (isDemo ? null : createClient()), [isDemo]); | ||||||||
| const { setUnreadCount, markThreadAsRead } = useUnreadMessages(); | ||||||||
| const realListing = isDemo ? null : (listing as ChatListing); | ||||||||
|
|
@@ -130,6 +157,24 @@ const ChatWindow = memo(function ChatWindow({ | |||||||
| const [messages, setMessages] = useState<ChatMessageRecord[]>( | ||||||||
| getThreadMessages(existingThread) | ||||||||
| ); | ||||||||
| const [clientTimeZone, setClientTimeZone] = useState<string | null>(null); | ||||||||
| const chatRenderOptions = useMemo<ChatRenderOptions>( | ||||||||
| () => | ||||||||
| isDemo | ||||||||
| ? { | ||||||||
| locale, | ||||||||
| now: DEMO_CHAT_REFERENCE_TIME, | ||||||||
| timeZone: CHAT_RENDER_TIME_ZONE, | ||||||||
| useRelativeDayLabels: clientTimeZone !== null, | ||||||||
| } | ||||||||
| : { | ||||||||
| ...defaultChatRenderOptions, | ||||||||
| locale, | ||||||||
| now: referenceNow, | ||||||||
| timeZone: clientTimeZone ?? CHAT_RENDER_TIME_ZONE, | ||||||||
|
||||||||
| timeZone: clientTimeZone ?? CHAT_RENDER_TIME_ZONE, | |
| timeZone: clientTimeZone ?? CHAT_RENDER_TIME_ZONE, | |
| useRelativeDayLabels: clientTimeZone !== null, |
Copilot
AI
Apr 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
showDateHeader recomputes getChatDateKey(...) (which uses Intl.DateTimeFormat(...).formatToParts) for both the current and previous message on every render. For long threads this becomes a hot path. Consider computing the current/previous date keys once per iteration (store in locals), or precomputing an array of date keys with useMemo when messages/timeZone change, then comparing adjacent entries.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -62,6 +62,7 @@ type ListingChatDrawerProps = { | |||||
| isChatDrawerOpen: boolean; | ||||||
| setIsChatDrawerOpen: (open: boolean) => void; | ||||||
| existingThread: ChatThreadRecord | null; | ||||||
| referenceNow?: string; | ||||||
|
||||||
| referenceNow?: string; | |
| referenceNow: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The JS-disabled
serverContexthard-codesbaseURLand doesn’t copy over the project’slocale/extraHTTPHeaders(seeplaywright.shared.ts), so the server-rendered snapshot could be in a different locale than the hydrated page. This can make the hydration regression test flaky or miss issues. Consider reusing the configured baseURL and passing the samelocale/extraHTTPHeadersintobrowser.newContext(...).