@sqlrooms/discuss
A simple discussion system for SQLRooms applications. Can be used for commenting and annotation with support for threaded conversations, real-time updates, and anchor-based discussions.
Overview
The @sqlrooms/discuss
module provides a complete discussion system with the following key features:
- Threaded conversations: Support for replies to discussions and comments
- Anchor-based discussions: Link discussions to specific data points or UI elements
- Real-time state management: Built on Zustand for reactive updates
- Customizable rendering: Flexible component system for custom UI implementations
- Delete confirmation: Built-in confirmation dialogs for safe content removal
- Highlighting: Visual highlighting of specific discussions
Main Components
Core Components
DiscussionList
The main container component that renders all discussions with built-in forms for adding, editing, and replying to content.
import {DiscussionList} from '@sqlrooms/discuss';
<DiscussionList
className="flex flex-col gap-4"
renderComment={(props) => <CustomCommentRenderer {...props} />}
renderDiscussion={(props) => <CustomDiscussionRenderer {...props} />}
/>;
CommentItem
Individual comment renderer with built-in edit/delete actions.
import {CommentItem} from '@sqlrooms/discuss';
<CommentItem comment={comment} discussion={discussion}>
<div className="flex flex-col gap-1">
<div className="text-muted-foreground text-xs">
{comment.userId} - {formatTimeRelative(comment.timestamp)}
</div>
<div className="whitespace-pre-wrap text-sm">{comment.text}</div>
</div>
</CommentItem>;
DiscussionItem
Container for a complete discussion thread including root comment and replies.
import {DiscussionItem} from '@sqlrooms/discuss';
<DiscussionItem
discussion={discussion}
renderComment={customCommentRenderer}
className="rounded border p-4"
/>;
Store Integration
Store Setup with createDiscussSlice
To use the discussion system, you need to integrate it with your project store using createDiscussSlice
:
import {
createDefaultDiscussConfig,
createDiscussSlice,
DiscussSliceConfig,
DiscussSliceState,
} from '@sqlrooms/discuss';
import {
BaseProjectConfig,
createProjectBuilderSlice,
createProjectBuilderStore,
ProjectBuilderState,
} from '@sqlrooms/project-builder';
import {z} from 'zod';
// 1. Extend your app config with DiscussSliceConfig
export const AppConfig = BaseProjectConfig.merge(DiscussSliceConfig);
export type AppConfig = z.infer<typeof AppConfig>;
// 2. Extend your app state with DiscussSliceState
export type AppState = ProjectBuilderState<AppConfig> & DiscussSliceState;
// 3. Create the store with discuss slice
export const {projectStore, useProjectStore} = createProjectBuilderStore<
AppConfig,
AppState
>((set, get, store) => ({
// Add the discuss slice with a user ID
...createDiscussSlice({userId: 'current-user-id'})(set, get, store),
// Add your project builder slice
...createProjectBuilderSlice<AppConfig>({
connector: yourDatabaseConnector,
config: {
// Include default discuss config
...createDefaultDiscussConfig(),
// Your other config...
layout: {
/* your layout */
},
dataSources: [
/* your data sources */
],
},
project: {
// Your project configuration
},
})(set, get, store),
}));
Using the Store Hook
Access discussion state and actions using the provided hook:
import {useStoreWithDiscussion} from '@sqlrooms/discuss';
function MyComponent() {
// Get discussions
const discussions = useStoreWithDiscussion(
(state) => state.config.discuss.discussions,
);
// Get actions
const addDiscussion = useStoreWithDiscussion(
(state) => state.discuss.addDiscussion,
);
const setReplyToItem = useStoreWithDiscussion(
(state) => state.discuss.setReplyToItem,
);
// Add a new discussion
const handleAddDiscussion = (text: string, anchorId?: string) => {
addDiscussion(text, anchorId);
};
// Start replying to a discussion
const handleReply = (discussionId: string) => {
setReplyToItem({discussionId});
};
}
Usage Examples
Basic Discussion Panel
import {DiscussionList, CommentItem} from '@sqlrooms/discuss';
import {useStoreWithDiscussion} from '@sqlrooms/discuss';
import {formatTimeRelative} from '@sqlrooms/utils';
const DiscussionPanel = () => {
const discussions = useStoreWithDiscussion(
(state) => state.config.discuss.discussions,
);
return (
<div className="h-full">
{discussions.length === 0 ? (
<div className="py-10 text-center text-gray-400">
<p>No comments yet. Start a discussion!</p>
</div>
) : (
<DiscussionList
className="flex flex-col gap-4"
renderComment={(props) => {
const {comment, discussion} = props;
const {anchorId} = discussion;
const isRootComment = comment.id === discussion.rootComment.id;
return (
<CommentItem {...props}>
<div className="flex flex-col gap-1">
{anchorId && isRootComment && (
<div className="text-md flex items-center gap-2 font-medium">
📍 Linked to: {anchorId}
</div>
)}
<div className="text-muted-foreground text-xs">
{comment.userId} - {formatTimeRelative(comment.timestamp)}
</div>
<div className="whitespace-pre-wrap text-sm">
{comment.text}
</div>
</div>
</CommentItem>
);
}}
/>
)}
</div>
);
};
Anchor-Based Discussions
Link discussions to specific data points or UI elements:
import {useStoreWithDiscussion} from '@sqlrooms/discuss';
function DataVisualization() {
const addDiscussion = useStoreWithDiscussion(
(state) => state.discuss.addDiscussion,
);
const setHighlightedDiscussionId = useStoreWithDiscussion(
(state) => state.discuss.setHighlightedDiscussionId,
);
// Handle clicking on a data point
const handleDataPointClick = (dataId: string) => {
// Add a discussion linked to this data point
addDiscussion('What do you think about this data point?', dataId);
};
// Highlight related discussion when hovering over data
const handleDataPointHover = (dataId: string) => {
const discussions =
useStoreWithDiscussion.getState().config.discuss.discussions;
const relatedDiscussion = discussions.find((d) => d.anchorId === dataId);
if (relatedDiscussion) {
setHighlightedDiscussionId(relatedDiscussion.id);
}
};
return (
<div>
{/* Your data visualization with clickable elements */}
<div
onClick={() => handleDataPointClick('airport-LAX')}
onMouseEnter={() => handleDataPointHover('airport-LAX')}
className="cursor-pointer hover:bg-blue-100"
>
LAX Airport
</div>
</div>
);
}
Custom Comment Rendering
import {DiscussionList, CommentItem} from '@sqlrooms/discuss';
import {Avatar, Badge} from '@sqlrooms/ui';
const CustomDiscussionPanel = () => {
return (
<DiscussionList
renderComment={(props) => {
const {comment, discussion} = props;
return (
<CommentItem {...props}>
<div className="flex gap-3">
<Avatar className="h-8 w-8">
{comment.userId.charAt(0).toUpperCase()}
</Avatar>
<div className="flex-1">
<div className="mb-1 flex items-center gap-2">
<span className="font-medium">{comment.userId}</span>
<Badge variant="secondary" className="text-xs">
{formatTimeRelative(comment.timestamp)}
</Badge>
</div>
<div className="text-sm leading-relaxed">{comment.text}</div>
</div>
</div>
</CommentItem>
);
}}
/>
);
};
Programmatic Discussion Management
import {useStoreWithDiscussion} from '@sqlrooms/discuss';
function DiscussionManager() {
const {
addDiscussion,
removeDiscussion,
addComment,
setReplyToItem,
setEditingItem,
submitEdit,
} = useStoreWithDiscussion((state) => state.discuss);
// Add a new discussion
const createDiscussion = () => {
addDiscussion('New discussion topic', 'optional-anchor-id');
};
// Reply to a discussion
const replyToDiscussion = (discussionId: string) => {
setReplyToItem({discussionId});
// User can now type in the form and call submitEdit
};
// Edit a comment
const editComment = (discussionId: string, commentId: string) => {
setEditingItem({discussionId, commentId});
// User can now edit in the form and call submitEdit
};
// Direct comment addition (bypassing UI state)
const addDirectComment = (discussionId: string, text: string) => {
addComment(discussionId, text);
};
return (
<div className="flex gap-2">
<button onClick={createDiscussion}>New Discussion</button>
<button onClick={() => replyToDiscussion('discussion-id')}>Reply</button>
<button onClick={() => editComment('discussion-id', 'comment-id')}>
Edit
</button>
</div>
);
}
API Reference
Types
Comment
: Individual comment with id, userId, text, timestamp, and optional parentIdDiscussion
: Container with id, optional anchorId, rootComment, and array of reply commentsDiscussSliceConfig
: Configuration type for the discuss sliceDiscussSliceState
: State type including all discussion actions and UI state
Key Functions
createDiscussSlice({ userId })
: Creates the discussion slice for your storecreateDefaultDiscussConfig()
: Returns default configuration for discussionsuseStoreWithDiscussion(selector)
: Hook to access discussion state and actions
Main Actions
submitEdit(text)
: Submit based on current UI state (add/reply/edit)addDiscussion(text, anchorId?)
: Add new discussionaddComment(discussionId, text, parentId?)
: Add reply to discussionsetReplyToItem(item)
: Set reply context for UIsetEditingItem(item)
: Set editing context for UIsetHighlightedDiscussionId(id)
: Highlight specific discussion
This module integrates seamlessly with the SQLRooms ecosystem and provides a complete foundation for building collaborative discussion features in your applications.