Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions src/web/src/app/c/channels/[serverId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useParams, useRouter, useSearchParams } from "next/navigation"
import { ServerLandingPendingFrame } from "@/components/community/shell/server-landing-pending-frame"
import { useServer } from "@/hooks/community/use-servers"
import { useBreakpoint } from "@/hooks/use-mobile"
import { getLastChannel, pickServerLandingChannel } from "@/lib/community/last-channel"
import { getLastChannel, resolveCommunityLandingHref } from "@/lib/community/last-channel"

export default function ServerDefaultPage() {
const params = useParams<{ serverId: string }>()
Expand All @@ -21,13 +21,15 @@ export default function ServerDefaultPage() {
const allChannels = currentServer.categories.flatMap((cat) => cat.channels)
// Restore one remembered channel id, or use the first top-level channel
// when there is no valid memory.
const target = pickServerLandingChannel(
allChannels.map((c) => c.id),
getLastChannel(serverId),
)
if (target) {
const target = resolveCommunityLandingHref({
serverId,
channelIds: allChannels.filter((channel) => !channel.pending).map((channel) => channel.id),
last: getLastChannel(serverId),
breakpoint,
})
if (target !== `/c/channels/${encodeURIComponent(serverId)}`) {
const search = searchParams.toString()
const href = `/c/channels/${serverId}/${target}${search ? `?${search}` : ""}`
const href = `${target}${search ? `?${search}` : ""}`
if (replacingHrefRef.current === href) return
replacingHrefRef.current = href
router.replace(href)
Expand Down
5 changes: 2 additions & 3 deletions src/web/src/app/c/layout.dom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,9 @@ describe("CommunityLayout session boundary", () => {
process.cwd().endsWith("/src/web") ? "" : "src/web",
"src/app/c/me/layout.tsx",
), "utf8")
expect(source).toMatch(/isPending:\s*dmsPending/)
expect(source).not.toContain("useDmRouteVerification")
expect(source).toContain('<DmRoute key={`${currentUser.id}/${params.dmId}`} dmId={params.dmId} />')
expect(source).not.toMatch(/isFetching:\s*dmsFetching/)
expect(source).toContain("const canonicalDmsUnsettled = dmsPending")
expect(source).toContain("useDmRouteVerification(params.dmId, dms, canonicalDmsUnsettled)")
})

it("keeps the daemon update controller inside the authenticated Community query cache", () => {
Expand Down
52 changes: 47 additions & 5 deletions src/web/src/app/c/me/[dmId]/page.dismissal.dom.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import React from "react"
import { render } from "@/test/react-dom-harness"
import { beforeEach, describe, expect, it, vi } from "vitest"
import DmPage from "./page"
import { DmView } from "@/components/community/channels/dm-view"

const {
mockDismissConversation,
mockCommitRoute,
mockDmMessages,
mockDms,
mockStore,
mockHistory,
} = vi.hoisted(() => ({
mockDismissConversation: vi.fn(),
mockCommitRoute: vi.fn(),
mockHistory: { allowed: true, error: null as Error | null, retry: vi.fn() },
mockDms: {
dms: [] as Array<{
id: string
Expand Down Expand Up @@ -38,12 +42,26 @@ vi.mock("@/components/community/channels/dm-header", () => ({ DmHeader: () => nu
vi.mock("@/components/community/channels/dm-loading-frame", () => ({
DmLoadingFrame: () => React.createElement("div", { "data-testid": "dm-loading" }),
}))
vi.mock("@/components/community/channels/dm-route-error-frame", () => ({
DmRouteErrorFrame: () => React.createElement("div", { "data-testid": "dm-error" }),
}))
vi.mock("@/components/community/channels/conversation-resolution-error-frame", () => ({
ConversationResolutionErrorFrame: ({ onRetry }: { onRetry: () => void }) => React.createElement("button", { onClick: onRetry, "data-testid": "history-error" }, "Retry"),
}))
vi.mock("@/hooks/community/use-channel-metadata", () => ({
useChannelMetadata: () => ({ isVerified: true, data: { historyVerification: mockHistory.allowed ? {} : undefined } }),
}))
vi.mock("@/hooks/community/channel-metadata", () => ({ isChannelMetadataTokenCurrent: () => true }))
vi.mock("@/lib/community/last-community-route", () => ({ commitCommunityChannelRoute: mockCommitRoute }))
vi.mock("@/components/community/avatar", () => ({ Avatar: () => null }))
vi.mock("@/components/community/messages/message-list", () => ({ MessageList: () => null }))
vi.mock("@/components/community/messages/message-list", () => ({ MessageList: () => React.createElement("div", { "data-testid": "history-body" }) }))
vi.mock("@/components/community/messages/message-context-sheet", () => ({
MessageContextSheet: () => null,
}))
vi.mock("@/components/community/messages/composer", () => ({ Composer: () => null }))
vi.mock("@/components/community/messages/composer", () => ({
Composer: () => React.createElement("div", { "data-testid": "composer" }),
ComposerSkeleton: () => React.createElement("div", { "data-testid": "composer-pending" }),
}))
vi.mock("@/components/community/messages/conversation-footer-shell", () => ({
ConversationFooterShell: ({ children }: { children: React.ReactNode }) => children,
ConversationFooterSlotProvider: ({ children }: { children: React.ReactNode }) => children,
Expand Down Expand Up @@ -84,7 +102,7 @@ vi.mock("@/hooks/community/use-messages", () => ({
}),
}))
vi.mock("@/hooks/community/use-dm-read-state", () => ({
useDmReadStateSnapshot: () => ({ snapshot: null, isFetching: false }),
useDmReadStateSnapshot: () => ({ snapshot: null, isFetching: false, error: mockHistory.error, retry: mockHistory.retry, retrying: false }),
}))
vi.mock("@/lib/community/message-read-projection", () => ({
resolveMessageReadProjection: () => ({ newDividerBefore: undefined, anchorFound: false }),
Expand Down Expand Up @@ -143,7 +161,11 @@ vi.mock("@/hooks/community/use-native-system-notifications", () => ({
describe("DM notification dismissal readiness", () => {
beforeEach(() => {
mockDismissConversation.mockClear()
mockCommitRoute.mockClear()
mockDmMessages.navigationBlocked = false
mockHistory.allowed = true
mockHistory.error = null
mockHistory.retry.mockClear()
mockDms.dms = []
mockDms.isLoading = false
})
Expand All @@ -164,12 +186,32 @@ describe("DM notification dismissal readiness", () => {
mockDms.isLoading = isLoading
mockDmMessages.navigationBlocked = navigationBlocked

render(React.createElement(DmPage))
render(React.createElement(DmView, { dmId: "dm_1" }))

expect(mockDismissConversation).toHaveBeenCalledExactlyOnceWith(
"viewer_1",
{ kind: "dm", channelId: "dm_1" },
expectedReady,
)
if (expectedReady) expect(mockCommitRoute).toHaveBeenCalledExactlyOnceWith("viewer_1", null, "dm_1")
else expect(mockCommitRoute).not.toHaveBeenCalled()
})

it("withholds history/composer and last while read access fails, then permits the same target after retry", () => {
mockDms.dms = [{ id: "dm_1", userId: "peer_1", name: "Peer", avatar: "P" }]
mockHistory.allowed = false
mockHistory.error = Object.assign(new Error("denied"), { status: 403 })
const view = render(React.createElement(DmView, { dmId: "dm_1" }))
expect(view.container.querySelector('[data-testid="history-error"]')).not.toBeNull()
expect(view.container.querySelector('[data-testid="history-body"]')).toBeNull()
expect(view.container.querySelector('[data-testid="composer"]')).toBeNull()
expect(mockCommitRoute).not.toHaveBeenCalled()
view.container.querySelector<HTMLButtonElement>('[data-testid="history-error"]')!.click()
expect(mockHistory.retry).toHaveBeenCalledOnce()
mockHistory.allowed = true
mockHistory.error = null
view.rerender(React.createElement(DmView, { dmId: "dm_1" }))
expect(view.container.querySelector('[data-testid="composer"]')).not.toBeNull()
expect(mockCommitRoute).toHaveBeenCalledExactlyOnceWith("viewer_1", null, "dm_1")
})
})
16 changes: 8 additions & 8 deletions src/web/src/app/c/me/[dmId]/page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { describe, expect, it } from "vitest"

describe("DM page loading ownership", () => {
it("keeps full-frame and message-body ownership separate", () => {
const source = readFileSync(new URL("./page.tsx", import.meta.url), "utf8")
const source = readFileSync(new URL("../../../../components/community/channels/dm-view.tsx", import.meta.url), "utf8")
expect(source).toContain("fullFramePending: !hasDm && dmsLoading")
expect(source).toContain("notFound: !hasDm && !dmsLoading")
expect(source).toContain("missingPeer: !hasDm && !dmsLoading")
expect(source).toContain("messageBodyLoading: hasDm && messagesLoading")
expect(source).not.toContain("currentChannelMatches")
expect(source).not.toContain("readSnapshotFetching ||\n messagesLoading")
Expand All @@ -26,20 +26,20 @@ describe("DM page loading ownership", () => {
const messageListProps = source.split("<MessageList\n")[1]?.split("/>")[0]
expect(messageListProps).toContain("typingUsers")
expect(source).not.toContain('data-onboarding-name={dm.name} className="shrink-0"')
expect(source).toContain("loading={loadingOwnership.messageBodyLoading}")
expect(source).not.toContain("<ComposerSkeleton")
expect(source).toContain("loading={!historyAllowed || loadingOwnership.messageBodyLoading}")
expect(source).toContain("!historyAllowed ? <ComposerSkeleton")
expect(source).not.toContain("<DmHeaderSkeleton")
})

it("uses the cache-first DM projection as the header and composer identity", () => {
const source = readFileSync(new URL("./page.tsx", import.meta.url), "utf8")
const source = readFileSync(new URL("../../../../components/community/channels/dm-view.tsx", import.meta.url), "utf8")
expect(source).toContain("dms.find((candidate) => candidate.id === dmId) ?? null")
expect(source).not.toContain("profilesByUserId.get(raw.userId)")
})

it("owns lazy channel-directory state and retry inside the keyed DM view", () => {
const source = readFileSync(new URL("./page.tsx", import.meta.url), "utf8")
expect(source).toContain("return <DmView key={params.dmId} />")
const source = readFileSync(new URL("../../../../components/community/channels/dm-view.tsx", import.meta.url), "utf8")
expect(source).toContain("export function DmView({ dmId }")
expect(source).toContain("useChannelRefDirectory(channelRefDirectoryEnabled)")
expect(source).toContain("loading: !channelRefDirectoryResolved")
expect(source).toContain("failed: channelRefDirectoryError")
Expand All @@ -50,7 +50,7 @@ describe("DM page loading ownership", () => {
})

it("keeps chip toggle and picker add on separate reaction intents", () => {
const source = readFileSync(new URL("./page.tsx", import.meta.url), "utf8")
const source = readFileSync(new URL("../../../../components/community/channels/dm-view.tsx", import.meta.url), "utf8")
expect(source).toContain("const toggleReaction = useToggleReactionApi()")
expect(source).toContain("const addReaction = useAddReactionApi()")
expect(source).toContain("onToggleReaction: (id: string, emoji: string) =>\n toggleReaction(")
Expand Down
Loading
Loading