Skip to content

Commit

Permalink
feat: Comments panel
Browse files Browse the repository at this point in the history
  • Loading branch information
areknawo committed Jun 10, 2024
1 parent 2e6522c commit 0bd2c7b
Show file tree
Hide file tree
Showing 29 changed files with 789 additions and 466 deletions.
4 changes: 2 additions & 2 deletions apps/web/src/components/fragments/collapsible-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ interface CollapsibleSectionProps {
children?: JSX.Element;
action?: JSX.Element;
gradient?: boolean;
collapsible?: boolean;
defaultOpened?: boolean;
mode?: "hide" | "remove";
}

const CollapsibleSection: Component<CollapsibleSectionProps> = (props) => {
Expand Down Expand Up @@ -68,7 +68,7 @@ const CollapsibleSection: Component<CollapsibleSectionProps> = (props) => {
opened() ? "py-1 my-1" : "max-h-0 overflow-hidden"
)}
>
{props.children}
<Show when={!props.mode || props.mode === "hide" || opened()}>{props.children}</Show>
</div>
</div>
);
Expand Down
8 changes: 1 addition & 7 deletions apps/web/src/components/fragments/mini-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,7 @@ const getExtensions = (options: ExtensionOptions): Extensions => {
const extensions = [
options.content ? Document.extend({ content: options.content }) : Document,
Placeholder.configure({
placeholder: ({ node, editor }) => {
if (node.type.name === "paragraph" && editor.state.doc.firstChild === node) {
return options.placeholder || "";
}

return "";
}
placeholder: options.placeholder
}),
Paragraph,
Text,
Expand Down
206 changes: 202 additions & 4 deletions apps/web/src/context/comments.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,211 @@
import { createContext, ParentComponent, useContext } from "solid-js";
import {
Accessor,
InitializedResource,
ParentComponent,
Setter,
createContext,
createEffect,
createMemo,
createResource,
createSignal,
on,
onCleanup,
useContext
} from "solid-js";
import { SetStoreFunction, createStore, reconcile } from "solid-js/store";
import { App, useClient, useContentData } from "#context";

interface CommentDataContextData {}
type ThreadWithFirstComment = Omit<App.CommentThread, "comments"> & {
firstComment: (Omit<App.Comment, "memberId"> & { member: App.CommentMember | null }) | null;
};
type UseCommentsOutput = {
comments: Accessor<Array<Omit<App.Comment, "memberId"> & { member: App.CommentMember | null }>>;
loading: Accessor<boolean>;
};
interface CommentFragmentData {
id: string;
top: Accessor<number>;
computedTop: Accessor<number>;
overlap: number;
pos: number;
size: number;
}
interface CommentDataProviderProps {}
interface CommentDataContextData {
fragments: Record<string, CommentFragmentData | undefined>;
orderedFragmentIds: Accessor<string[]>;
threads: InitializedResource<ThreadWithFirstComment[]>;
setOrderedFragmentIds: Setter<string[]>;
setFragments: SetStoreFunction<Record<string, CommentFragmentData | undefined>>;
activeFragmentId: Accessor<string | null>;
setActiveFragmentId: Setter<string | null>;
getThreadByFragment(fragmentId: string): ThreadWithFirstComment | null;
getCommentNumbers(fragmentId: string): number;
useCommentsInFragment(fragmentId: Accessor<string>): UseCommentsOutput;
useCommentsInThread(threadId: Accessor<string>): UseCommentsOutput;
}

const CommentDataContext = createContext<CommentDataContextData>();
const CommentDataProvider: ParentComponent = (props) => {
return <CommentDataContext.Provider value={{}}>{props.children}</CommentDataContext.Provider>;
const CommentDataProvider: ParentComponent<CommentDataProviderProps> = (props) => {
const { activeContentPieceId } = useContentData();
const client = useClient();
const [orderedFragmentIds, setOrderedFragmentIds] = createSignal<string[]>([]);
const [activeFragmentId, setActiveFragmentId] = createSignal<string | null>(null);
const [fragments, setFragments] = createStore<Record<string, CommentFragmentData | undefined>>(
{}
);
const [loadingComments, setLoadingComments] = createStore<Record<string, boolean | undefined>>(
{}
);
const [threads, { mutate: setThreads }] = createResource(
() => {
if (!activeContentPieceId()) return [];

return client.comments.listThreads.query({
contentPieceId: activeContentPieceId()!
});
},
{ initialValue: [] }
);
const [commentsByFragmentId, setCommentsByFragmentId] = createStore<
Record<
string,
Array<Omit<App.Comment, "memberId"> & { member: App.CommentMember | null }> | undefined
>
>({});
const useCommentsInFragment = (fragmentId: Accessor<string>): UseCommentsOutput => {
createEffect(
on(fragmentId, (fragmentId, previousFragmentId) => {
if (fragmentId && fragmentId !== previousFragmentId && !commentsByFragmentId[fragmentId]) {
setLoadingComments(fragmentId, true);
client.comments.listComments
.query({
fragment: fragmentId
})
.then((comments) => {
setCommentsByFragmentId(fragmentId, comments);
setLoadingComments(fragmentId, undefined);
})
.finally(() => {
setLoadingComments(fragmentId, undefined);
});
}
})
);

return {
comments: () => commentsByFragmentId[fragmentId()] || [],
loading: () => loadingComments[fragmentId()] || false
};
};
const useCommentsInThread = (threadId: Accessor<string>): UseCommentsOutput => {
const fragmentId = createMemo(() => {
return threads().find((thread) => thread.id === threadId())?.fragment || "";
});

return useCommentsInFragment(fragmentId);
};
const getThreadByFragment = (fragmentId: string): ThreadWithFirstComment | null => {
return threads().find((thread) => thread.fragment === fragmentId) || null;
};
const getCommentNumbers = (fragmentId: string): number => {
return commentsByFragmentId[fragmentId]?.length || 0;
};

createEffect(
on(activeContentPieceId, () => {
const commentChangesSubscription = client.comments.changes.subscribe(
{ contentPieceId: activeContentPieceId()! },
{
onData({ action, data }) {
if (action.includes("Comment")) {
const thread = threads().find((thread) => {
return "threadId" in data && thread.id === data.threadId;
});

if (action === "createComment" && thread && thread.fragment in commentsByFragmentId) {
return setCommentsByFragmentId(thread.fragment, (comments) => [
...(comments || []),
data
]);
}

if (action === "updateComment" && thread && thread.fragment in commentsByFragmentId) {
return setCommentsByFragmentId(thread.fragment, (comments) => {
return (comments || []).map((comment) => {
if (comment.id === data.id) {
return { ...comment, content: data.content };
}

return comment;
});
});
}

if (action === "deleteComment" && thread && thread.fragment in commentsByFragmentId) {
return setCommentsByFragmentId(thread.fragment, (comments) => {
return (comments || []).filter((comment) => comment.id !== data.id);
});
}
}

if (action === "createThread") {
return setThreads((threads) => [...threads, { ...data, firstComment: null }]);
}

if (action === "resolveThread") {
return setThreads((threads) => {
return threads.map((thread) => {
if (thread.id === data.id) {
return { ...thread, resolved: data.resolved };
}

return thread;
});
});
}

if (action === "deleteThread") {
setCommentsByFragmentId(data.fragment, undefined);

return setThreads((threads) => {
return threads.filter((thread) => thread.id !== data.id);
});
}
}
}
);

onCleanup(() => {
commentChangesSubscription.unsubscribe();
setCommentsByFragmentId(reconcile({}));
});
})
);

return (
<CommentDataContext.Provider
value={{
useCommentsInFragment,
useCommentsInThread,
orderedFragmentIds,
getThreadByFragment,
getCommentNumbers,
activeFragmentId,
setOrderedFragmentIds,
setActiveFragmentId,
fragments,
threads,
setFragments
}}
>
{props.children}
</CommentDataContext.Provider>
);
};
const useCommentData = (): CommentDataContextData => {
return useContext(CommentDataContext)!;
};

export { CommentDataProvider, useCommentData };
export type { ThreadWithFirstComment, CommentFragmentData, CommentDataContextData };
121 changes: 62 additions & 59 deletions apps/web/src/layout/secured-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { IconButton, Tooltip } from "#components/primitives";
import { SubscriptionBanner } from "#ee";
import { SnippetsDataProvider } from "#context/snippets";
import { CommentDataProvider } from "#context/comments";

const Analytics: Component = () => {
const { profile, workspace } = useAuthenticatedUserData();
Expand Down Expand Up @@ -60,75 +61,77 @@ const SecuredLayout: ParentComponent = (props) => {
<AppearanceProvider>
<ExtensionsProvider>
<ContentDataProvider>
<SnippetsDataProvider>
<CommandPaletteProvider>
<WalkthroughProvider>
<div class="flex flex-col h-full w-full">
<Show when={hostConfig.billing}>
<SubscriptionBanner />
</Show>
<div
class={clsx(
"flex-1 flex flex-col-reverse md:flex-row h-[calc(100%-1.5rem)]",
!storage().zenMode && "md:border-b-2 border-gray-200 dark:border-gray-700"
)}
>
<Show
when={!storage().zenMode}
fallback={
<Tooltip
wrapperClass="fixed top-0 right-0 z-50 mt-2 md:mt-4 mr-4 md:mr-6"
class="-ml-1"
text="Exit Zen mode"
side="left"
>
<IconButton
path={mdiFullscreenExit}
class="m-0"
text="soft"
onClick={() => {
setStorage((storage) => ({ ...storage, zenMode: false }));
}}
/>
</Tooltip>
}
>
<SidebarMenu />
<CommentDataProvider>
<SnippetsDataProvider>
<CommandPaletteProvider>
<WalkthroughProvider>
<div class="flex flex-col h-full w-full">
<Show when={hostConfig.billing}>
<SubscriptionBanner />
</Show>
<div
class="flex flex-col flex-1 md:h-full overflow-visible"
id="main-scrollable-container"
class={clsx(
"flex-1 flex flex-col-reverse md:flex-row h-[calc(100%-1.5rem)]",
!storage().zenMode && "md:border-b-2 border-gray-200 dark:border-gray-700"
)}
>
<div class="flex flex-1 h-full">
<Show when={!storage().zenMode}>
<SidePanel />
</Show>
<div class="flex-1 relative flex flex-col w-full">
<Show
when={!storage().zenMode}
fallback={
<Tooltip
wrapperClass="fixed top-0 right-0 z-50 mt-2 md:mt-4 mr-4 md:mr-6"
class="-ml-1"
text="Exit Zen mode"
side="left"
>
<IconButton
path={mdiFullscreenExit}
class="m-0"
text="soft"
onClick={() => {
setStorage((storage) => ({ ...storage, zenMode: false }));
}}
/>
</Tooltip>
}
>
<SidebarMenu />
</Show>
<div
class="flex flex-col flex-1 md:h-full overflow-visible"
id="main-scrollable-container"
>
<div class="flex flex-1 h-full">
<Show when={!storage().zenMode}>
<Toolbar />
<SidePanel />
</Show>
<div
class={clsx(
"absolute w-full",
storage().zenMode ? "top-0 h-full" : "h-[calc(100%-3rem)] top-12"
)}
>
{props.children}
<div class="flex-1 relative flex flex-col w-full">
<Show when={!storage().zenMode}>
<Toolbar />
</Show>
<div
class={clsx(
"absolute w-full",
storage().zenMode ? "top-0 h-full" : "h-[calc(100%-3rem)] top-12"
)}
>
{props.children}
</div>
</div>
<Show when={!storage().zenMode}>
<SidePanelRight />
</Show>
</div>
<Show when={!storage().zenMode}>
<SidePanelRight />
</Show>
</div>
</div>
<Show when={!storage().zenMode}>
<BottomMenu />
</Show>
</div>
<Show when={!storage().zenMode}>
<BottomMenu />
</Show>
</div>
</WalkthroughProvider>
</CommandPaletteProvider>
</SnippetsDataProvider>
</WalkthroughProvider>
</CommandPaletteProvider>
</SnippetsDataProvider>
</CommentDataProvider>
</ContentDataProvider>
</ExtensionsProvider>
</AppearanceProvider>
Expand Down
Loading

0 comments on commit 0bd2c7b

Please sign in to comment.