-
Notifications
You must be signed in to change notification settings - Fork 4.2k
feat: complete community Magic Board across desktop and mobile #5476
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
base: main
Are you sure you want to change the base?
Changes from all commits
9c79d27
47da549
6637f92
a89ea28
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 |
|---|---|---|
|
|
@@ -4,9 +4,21 @@ import { useQueryClient } from "@tanstack/react-query"; | |
| import type { SearchHighlightNavigation } from "@/app/navigation/searchHighlightNavigation"; | ||
| import { getCachedSearchHitEvent } from "@/app/navigation/searchHitEventCache"; | ||
| import { useAppNavigation } from "@/app/navigation/useAppNavigation"; | ||
| import { useChannelsQuery } from "@/features/channels/hooks"; | ||
| import { | ||
| useCanvasQuery, | ||
| useCanvasSubscription, | ||
| useChannelsQuery, | ||
| } from "@/features/channels/hooks"; | ||
| import { useOpenChannelDirectoryQuery } from "@/features/channels/openChannelDirectory"; | ||
| import { | ||
| type ChannelViewMode, | ||
| readStoredChannelViewMode, | ||
| resolveChannelViewMode, | ||
| writeStoredChannelViewMode, | ||
| } from "@/features/channels/lib/canvasBoard"; | ||
| import { ChannelBoardScreen } from "@/features/channels/ui/ChannelBoardScreen"; | ||
| import { ChannelScreen } from "@/features/channels/ui/ChannelScreen"; | ||
| import { ChannelViewModeProvider } from "@/features/channels/ui/ChannelViewModeContext"; | ||
| import { HuddleStartingView } from "@/features/huddle/components/HuddleStartingView"; | ||
| import { huddleWindowChannelId } from "@/features/huddle/lib/huddleWindow"; | ||
| import { | ||
|
|
@@ -34,6 +46,7 @@ type ChannelRouteScreenProps = { | |
| autoSendDraftKey: string | null; | ||
| channelId: string; | ||
| searchHighlight: SearchHighlightNavigation | null | undefined; | ||
| hasStreamRouteIntent: boolean; | ||
| selectedPostId: string | null; | ||
| targetMessageId: string | null; | ||
| targetReplyId: string | null; | ||
|
|
@@ -115,6 +128,7 @@ export function ChannelRouteScreen({ | |
| autoSendDraftKey, | ||
| channelId, | ||
| searchHighlight, | ||
| hasStreamRouteIntent, | ||
| selectedPostId, | ||
| targetMessageId, | ||
| targetReplyId, | ||
|
|
@@ -159,6 +173,44 @@ export function ChannelRouteScreen({ | |
| ); | ||
| const projectHome = | ||
| enumeratedProjectHome ?? projectHomeLookupQuery.data ?? null; | ||
| const canvasQuery = useCanvasQuery( | ||
| activeChannel?.id ?? null, | ||
| activeChannel !== null && activeChannel.channelType !== "dm", | ||
| ); | ||
| useCanvasSubscription( | ||
| activeChannel?.id ?? null, | ||
| activeChannel !== null && activeChannel.channelType !== "dm", | ||
| ); | ||
| const [viewSelection, setViewSelection] = React.useState<{ | ||
| channelId: string; | ||
| mode: ChannelViewMode; | ||
| } | null>(null); | ||
| const hasRouteTarget = Boolean( | ||
| hasStreamRouteIntent || | ||
| searchHighlight || | ||
| selectedPostId || | ||
| targetMessageId || | ||
|
Comment on lines
+188
to
+192
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In a Dispatch channel or any channel whose saved preference is Board, a Useful? React with 👍 / 👎. |
||
| targetReplyId || | ||
| targetThreadRootId, | ||
| ); | ||
| const explicitView = | ||
| viewSelection?.channelId === channelId | ||
| ? viewSelection.mode | ||
| : readStoredChannelViewMode(channelId); | ||
| const channelView = resolveChannelViewMode({ | ||
| channelName: activeChannel?.name ?? null, | ||
| channelType: activeChannel?.channelType ?? null, | ||
| explicitView, | ||
| hasCanvas: Boolean(canvasQuery.data?.content?.trim()), | ||
| hasRouteTarget, | ||
| }); | ||
| const handleViewModeChange = React.useCallback( | ||
| (mode: ChannelViewMode) => { | ||
| setViewSelection({ channelId, mode }); | ||
| writeStoredChannelViewMode(channelId, mode); | ||
| }, | ||
| [channelId], | ||
| ); | ||
| const [targetMessageEvents, setTargetMessageEvents] = React.useState< | ||
| RelayEvent[] | ||
| >(() => { | ||
|
|
@@ -309,23 +361,41 @@ export function ChannelRouteScreen({ | |
| } | ||
|
|
||
| return ( | ||
| <ChannelScreen | ||
| activeChannel={activeChannel} | ||
| autoSendDraftKey={autoSendDraftKey} | ||
| currentIdentity={identityQuery.data} | ||
| currentProfile={profileQuery.data} | ||
| onCloseForumPost={() => { | ||
| void closeForumPost(channelId); | ||
| }} | ||
| onSelectForumPost={(postId) => { | ||
| void goForumPost(channelId, postId); | ||
| <ChannelViewModeProvider | ||
| value={{ | ||
| boardAvailable: channelView.boardAvailable && !hasRouteTarget, | ||
| mode: channelView.mode, | ||
| onModeChange: handleViewModeChange, | ||
| }} | ||
| selectedForumPostId={selectedPostId} | ||
| targetForumReplyId={targetReplyId} | ||
| targetMessageEvents={targetMessageEvents} | ||
| targetMessageId={targetMessageId} | ||
| targetSearchMessageId={activeSearchHighlight?.messageId} | ||
| targetSearchQuery={activeSearchHighlight?.query} | ||
| /> | ||
| > | ||
| {channelView.mode === "board" && activeChannel ? ( | ||
| <ChannelBoardScreen | ||
| canvas={canvasQuery.data} | ||
| canvasError={canvasQuery.error} | ||
| canvasLoading={canvasQuery.isLoading} | ||
| channel={activeChannel} | ||
| currentPubkey={identityQuery.data?.pubkey} | ||
| /> | ||
| ) : ( | ||
| <ChannelScreen | ||
| activeChannel={activeChannel} | ||
| autoSendDraftKey={autoSendDraftKey} | ||
| currentIdentity={identityQuery.data} | ||
| currentProfile={profileQuery.data} | ||
| onCloseForumPost={() => { | ||
| void closeForumPost(channelId); | ||
| }} | ||
| onSelectForumPost={(postId) => { | ||
| void goForumPost(channelId, postId); | ||
| }} | ||
| selectedForumPostId={selectedPostId} | ||
| targetForumReplyId={targetReplyId} | ||
| targetMessageEvents={targetMessageEvents} | ||
| targetMessageId={targetMessageId} | ||
| targetSearchMessageId={activeSearchHighlight?.messageId} | ||
| targetSearchQuery={activeSearchHighlight?.query} | ||
| /> | ||
| )} | ||
| </ChannelViewModeProvider> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,11 +27,14 @@ import { | |
| unarchiveChannel, | ||
| updateChannel, | ||
| } from "@/shared/api/tauri"; | ||
| import { relayClient } from "@/shared/api/relayClient"; | ||
| import type { | ||
| AddChannelMembersInput, | ||
| CanvasResponse, | ||
| Channel, | ||
| ChannelDetail, | ||
| CreateChannelInput, | ||
| RelayEvent, | ||
| SetChannelPurposeInput, | ||
| SetChannelTopicInput, | ||
| UpdateChannelInput, | ||
|
|
@@ -976,15 +979,94 @@ export function useCanvasQuery(channelId: string | null, enabled = true) { | |
| }); | ||
| } | ||
|
|
||
| export function useCanvasSubscription( | ||
| channelId: string | null, | ||
| enabled = true, | ||
| ) { | ||
| const queryClient = useQueryClient(); | ||
|
|
||
| React.useEffect(() => { | ||
| if (!enabled || !channelId) { | ||
| return; | ||
| } | ||
| let isDisposed = false; | ||
| let cleanup: (() => void) | undefined; | ||
| const applyCanvasEvent = (event: RelayEvent) => { | ||
| queryClient.setQueryData<CanvasResponse>( | ||
| ["channel-canvas", channelId], | ||
| (current) => { | ||
| if (current?.updatedAt && current.updatedAt > event.created_at) { | ||
| return current; | ||
| } | ||
| return { | ||
| author: event.pubkey, | ||
| content: event.content, | ||
| eventId: event.id, | ||
| updatedAt: event.created_at, | ||
| }; | ||
| }, | ||
| ); | ||
| }; | ||
| void relayClient | ||
| .subscribeLive( | ||
| { | ||
| "#h": [channelId], | ||
| kinds: [40100], | ||
| limit: 0, | ||
|
Comment on lines
+1011
to
+1015
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the initial AGENTS.md reference: AGENTS.md:L183-L186 Useful? React with 👍 / 👎. |
||
| }, | ||
| applyCanvasEvent, | ||
| ) | ||
| .then((dispose) => { | ||
| if (isDisposed) { | ||
| dispose(); | ||
| return; | ||
| } | ||
| cleanup = dispose; | ||
| }) | ||
| .catch((error) => { | ||
| console.error( | ||
| "Failed to subscribe to channel canvas", | ||
| channelId, | ||
| error, | ||
| ); | ||
| }); | ||
| const disposeReconnect = relayClient.subscribeToReconnects(() => { | ||
| void queryClient.invalidateQueries({ | ||
| queryKey: ["channel-canvas", channelId], | ||
| }); | ||
| }); | ||
| return () => { | ||
| isDisposed = true; | ||
| cleanup?.(); | ||
| disposeReconnect(); | ||
| }; | ||
| }, [channelId, enabled, queryClient]); | ||
| } | ||
|
|
||
| export function useSetCanvasMutation(channelId: string | null) { | ||
| const queryClient = useQueryClient(); | ||
|
|
||
| return useMutation({ | ||
| mutationFn: (content: string) => { | ||
| mutationFn: ( | ||
| input: | ||
| | string | ||
| | { | ||
| content: string; | ||
| enforceRevision?: boolean; | ||
| expectedEventId?: string | null; | ||
| }, | ||
| ) => { | ||
| if (!channelId) { | ||
| return Promise.reject(new Error("No channel selected")); | ||
| } | ||
| return setCanvas({ channelId, content }); | ||
| return setCanvas({ | ||
| channelId, | ||
| content: typeof input === "string" ? input : input.content, | ||
| enforceRevision: | ||
| typeof input === "string" ? false : input.enforceRevision, | ||
| expectedEventId: | ||
| typeof input === "string" ? undefined : input.expectedEventId, | ||
| }); | ||
| }, | ||
| onSuccess: () => { | ||
| if (channelId) { | ||
|
|
||
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.
When multiple board mutations occur within one second, their signed events share the same second-resolution
created_at; however,query_eventsorders ties byid ASC(crates/buzz-db/src/store/event.rs:685-693). After the client caches the returned ID of a later drag or status write, this limit-one query can still select the earlier lower-ID event, causing the next save to report a revision conflict despite no concurrent editor and causing reloads to select the earlier canvas. The revision must use an authoritative monotonically ordered token rather than assuming the last submitted event ID will become the query head.Useful? React with 👍 / 👎.