@liveblocks/react-ui 3.21.0-exp9 → 3.21.0-private2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Comment.cjs +5 -1
- package/dist/components/Comment.cjs.map +1 -1
- package/dist/components/Comment.js +6 -2
- package/dist/components/Comment.js.map +1 -1
- package/dist/components/Composer.cjs +10 -1
- package/dist/components/Composer.cjs.map +1 -1
- package/dist/components/Composer.js +10 -1
- package/dist/components/Composer.js.map +1 -1
- package/dist/components/Thread.cjs +4 -1
- package/dist/components/Thread.cjs.map +1 -1
- package/dist/components/Thread.js +4 -1
- package/dist/components/Thread.js.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/shared.cjs +10 -0
- package/dist/shared.cjs.map +1 -1
- package/dist/shared.js +10 -1
- package/dist/shared.js.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Composer.cjs","sources":["../../src/components/Composer.tsx"],"sourcesContent":["\"use client\";\n\nimport type {\n BaseMetadata,\n CommentAttachment,\n CommentMixedAttachment,\n DCM,\n DTM,\n GroupMentionData,\n Patchable,\n} from \"@liveblocks/core\";\nimport { assertNever, MENTION_CHARACTER } from \"@liveblocks/core\";\nimport { useRoom } from \"@liveblocks/react\";\nimport {\n useCreateRoomComment,\n useCreateRoomThread,\n useEditRoomComment,\n useHasPermissionAccess,\n useLayoutEffect,\n useResolveMentionSuggestions,\n} from \"@liveblocks/react/_private\";\nimport type {\n ComponentPropsWithoutRef,\n ComponentType,\n FocusEvent,\n FormEvent,\n ForwardedRef,\n MouseEvent,\n PropsWithChildren,\n ReactNode,\n RefAttributes,\n SyntheticEvent,\n} from \"react\";\nimport { createContext, forwardRef, useCallback, useMemo, useRef } from \"react\";\n\nimport { useLiveblocksUiConfig } from \"../config\";\nimport { FLOATING_ELEMENT_SIDE_OFFSET } from \"../constants\";\nimport { UsersIcon } from \"../icons\";\nimport { AttachmentIcon } from \"../icons/Attachment\";\nimport { BoldIcon } from \"../icons/Bold\";\nimport { CodeIcon } from \"../icons/Code\";\nimport { EmojiIcon } from \"../icons/Emoji\";\nimport { ItalicIcon } from \"../icons/Italic\";\nimport { MentionIcon } from \"../icons/Mention\";\nimport { SendIcon } from \"../icons/Send\";\nimport { StrikethroughIcon } from \"../icons/Strikethrough\";\nimport type { ComposerOverrides, GlobalOverrides } from \"../overrides\";\nimport { useOverrides } from \"../overrides\";\nimport * as ComposerPrimitive from \"../primitives/Composer\";\nimport {\n useComposer,\n useComposerAttachmentsContext,\n useComposerEditorContext,\n} from \"../primitives/Composer/contexts\";\nimport type {\n ComposerEditorComponents,\n ComposerEditorLinkProps,\n ComposerEditorMentionProps,\n ComposerEditorMentionSuggestionsProps,\n ComposerEditorProps,\n ComposerFormProps,\n ComposerMarkToggleProps,\n ComposerSubmitComment,\n} from \"../primitives/Composer/types\";\nimport { useComposerAttachmentsDropArea } from \"../primitives/Composer/utils\";\nimport type { ComposerBodyMark } from \"../types\";\nimport { cn } from \"../utils/cn\";\nimport { useControllableState } from \"../utils/use-controllable-state\";\nimport { useIsGroupMentionMember } from \"../utils/use-group-mention\";\nimport { GroupAvatar, UserAvatar } from \"./Avatar\";\nimport { FileAttachment } from \"./internal/Attachment\";\nimport { Attribution } from \"./internal/Attribution\";\nimport { Button } from \"./internal/Button\";\nimport type { EmojiPickerProps } from \"./internal/EmojiPicker\";\nimport { EmojiPicker, EmojiPickerTrigger } from \"./internal/EmojiPicker\";\nimport { Group } from \"./internal/Group\";\nimport { GroupDescription } from \"./internal/GroupDescription\";\nimport { ShortcutTooltip, Tooltip, TooltipProvider } from \"./internal/Tooltip\";\nimport { User } from \"./internal/User\";\n\ninterface EditorActionProps extends ComponentPropsWithoutRef<\"button\"> {\n label: string;\n tooltipLabel?: string;\n}\n\ninterface EmojiEditorActionProps extends EditorActionProps {\n onPickerOpenChange?: EmojiPickerProps[\"onOpenChange\"];\n}\n\ninterface MarkToggleProps extends ComposerMarkToggleProps {\n icon?: ReactNode;\n shortcut?: string;\n}\n\nexport type ComposerCreateThreadProps<\n TM extends BaseMetadata,\n CM extends BaseMetadata,\n> = {\n threadId?: never;\n\n commentId?: never;\n\n /**\n * The metadata of the thread to create.\n */\n metadata?: TM;\n\n /**\n * The metadata of the comment to create.\n */\n commentMetadata?: CM;\n};\n\nexport type ComposerCreateCommentProps<CM extends BaseMetadata> = {\n /**\n * The ID of the thread to reply to.\n */\n threadId: string;\n\n commentId?: never;\n\n metadata?: never;\n\n /**\n * The metadata of the comment to create.\n */\n commentMetadata?: CM;\n};\n\nexport type ComposerEditCommentProps<CM extends BaseMetadata> = {\n /**\n * The ID of the thread to edit a comment in.\n */\n threadId: string;\n\n /**\n * The ID of the comment to edit.\n */\n commentId: string;\n\n metadata?: never;\n\n /**\n * The metadata of the comment to edit.\n */\n commentMetadata?: Patchable<CM>;\n};\n\nexport type ComposerProps<\n TM extends BaseMetadata = DTM,\n CM extends BaseMetadata = DCM,\n> = Omit<ComponentPropsWithoutRef<\"form\">, \"defaultValue\"> &\n (\n | ComposerCreateThreadProps<TM, CM>\n | ComposerCreateCommentProps<CM>\n | ComposerEditCommentProps<CM>\n ) & {\n /**\n * The event handler called when the composer is submitted.\n */\n onComposerSubmit?: (\n comment: ComposerSubmitComment,\n event: FormEvent<HTMLFormElement>\n ) => Promise<void> | void;\n\n /**\n * The composer's initial value.\n */\n defaultValue?: ComposerEditorProps[\"defaultValue\"];\n\n /**\n * The composer's initial attachments.\n */\n defaultAttachments?: CommentAttachment[];\n\n /**\n * Whether the composer is collapsed. Setting a value will make the composer controlled.\n */\n collapsed?: boolean;\n\n /**\n * The event handler called when the collapsed state of the composer changes.\n */\n onCollapsedChange?: (collapsed: boolean) => void;\n\n /**\n * Whether the composer is initially collapsed. Setting a value will make the composer uncontrolled.\n */\n defaultCollapsed?: boolean;\n\n /**\n * Whether to show and allow adding attachments.\n */\n showAttachments?: boolean;\n\n /**\n * Whether to show formatting controls (e.g. a floating toolbar with formatting toggles when selecting text)\n */\n showFormattingControls?: boolean;\n\n /**\n * Whether the composer is disabled.\n */\n disabled?: ComposerFormProps[\"disabled\"];\n\n /**\n * Whether to focus the composer on mount.\n */\n autoFocus?: ComposerEditorProps[\"autoFocus\"];\n\n /**\n * Whether to blur the composer editor when the composer is submitted.\n */\n blurOnSubmit?: boolean;\n\n /**\n * Override the component's strings.\n */\n overrides?: Partial<GlobalOverrides & ComposerOverrides>;\n\n /**\n * @internal\n */\n actions?: ReactNode;\n\n /**\n * @internal\n */\n showAttribution?: boolean;\n\n /**\n * @internal\n */\n roomId?: string;\n };\n\ninterface ComposerEditorContainerProps extends Pick<\n ComposerProps,\n | \"defaultValue\"\n | \"showAttachments\"\n | \"showFormattingControls\"\n | \"showAttribution\"\n | \"overrides\"\n | \"actions\"\n | \"autoFocus\"\n | \"disabled\"\n> {\n isCollapsed: boolean | undefined;\n onEmptyChange: (isEmpty: boolean) => void;\n hasResolveMentionSuggestions: boolean;\n onEmojiPickerOpenChange: (isOpen: boolean) => void;\n onEditorClick: (event: MouseEvent<HTMLDivElement>) => void;\n}\n\ninterface ComposerMentionProps extends ComposerEditorMentionProps {\n overrides?: ComposerProps[\"overrides\"];\n}\n\nfunction ComposerInsertMentionEditorAction({\n label,\n tooltipLabel,\n className,\n onClick,\n ...props\n}: EditorActionProps) {\n const { createMention } = useComposer();\n\n const preventDefault = useCallback((event: SyntheticEvent) => {\n event.preventDefault();\n }, []);\n\n const handleClick = useCallback(\n (event: MouseEvent<HTMLButtonElement>) => {\n onClick?.(event);\n\n if (!event.isDefaultPrevented()) {\n event.stopPropagation();\n createMention();\n }\n },\n [createMention, onClick]\n );\n\n return (\n <Tooltip content={tooltipLabel ?? label}>\n <Button\n className={cn(\"lb-composer-editor-action\", className)}\n onPointerDown={preventDefault}\n onClick={handleClick}\n aria-label={label}\n icon={<MentionIcon />}\n {...props}\n />\n </Tooltip>\n );\n}\n\nfunction ComposerInsertEmojiEditorAction({\n label,\n tooltipLabel,\n onPickerOpenChange,\n className,\n ...props\n}: EmojiEditorActionProps) {\n const { insertText } = useComposer();\n\n const preventDefault = useCallback((event: SyntheticEvent) => {\n event.preventDefault();\n }, []);\n\n const stopPropagation = useCallback((event: SyntheticEvent) => {\n event.stopPropagation();\n }, []);\n\n return (\n <EmojiPicker onEmojiSelect={insertText} onOpenChange={onPickerOpenChange}>\n <Tooltip content={tooltipLabel ?? label}>\n <EmojiPickerTrigger asChild>\n <Button\n className={cn(\"lb-composer-editor-action\", className)}\n onPointerDown={preventDefault}\n onClick={stopPropagation}\n aria-label={label}\n icon={<EmojiIcon />}\n {...props}\n />\n </EmojiPickerTrigger>\n </Tooltip>\n </EmojiPicker>\n );\n}\n\nfunction ComposerAttachFilesEditorAction({\n label,\n tooltipLabel,\n className,\n ...props\n}: EditorActionProps) {\n const preventDefault = useCallback((event: SyntheticEvent) => {\n event.preventDefault();\n }, []);\n\n const stopPropagation = useCallback((event: SyntheticEvent) => {\n event.stopPropagation();\n }, []);\n\n return (\n <Tooltip content={tooltipLabel ?? label}>\n <ComposerPrimitive.AttachFiles asChild>\n <Button\n className={cn(\"lb-composer-editor-action\", className)}\n onPointerDown={preventDefault}\n onClick={stopPropagation}\n aria-label={label}\n icon={<AttachmentIcon />}\n {...props}\n />\n </ComposerPrimitive.AttachFiles>\n </Tooltip>\n );\n}\n\nfunction ComposerUserMention({ mention }: ComposerMentionProps) {\n return (\n <ComposerPrimitive.Mention className=\"lb-mention lb-composer-mention\">\n <span className=\"lb-mention-symbol\">{MENTION_CHARACTER}</span>\n <User userId={mention.id} />\n </ComposerPrimitive.Mention>\n );\n}\n\nfunction ComposerGroupMention({ mention }: ComposerMentionProps) {\n const isMember = useIsGroupMentionMember(mention as GroupMentionData);\n\n return (\n <ComposerPrimitive.Mention\n className=\"lb-mention lb-composer-mention\"\n data-self={isMember ? \"\" : undefined}\n >\n <span className=\"lb-mention-symbol\">{MENTION_CHARACTER}</span>\n <Group groupId={mention.id} />\n </ComposerPrimitive.Mention>\n );\n}\n\nexport function ComposerMention({ mention, ...props }: ComposerMentionProps) {\n switch (mention.kind) {\n case \"user\":\n return <ComposerUserMention mention={mention} {...props} />;\n\n case \"group\":\n return <ComposerGroupMention mention={mention} {...props} />;\n\n default:\n return assertNever(mention, \"Unhandled mention kind\");\n }\n}\n\nfunction ComposerMentionSuggestions({\n mentions,\n}: ComposerEditorMentionSuggestionsProps) {\n return mentions.length > 0 ? (\n <ComposerPrimitive.Suggestions className=\"lb-root lb-portal lb-elevation lb-composer-suggestions lb-composer-mention-suggestions\">\n <ComposerPrimitive.SuggestionsList className=\"lb-composer-suggestions-list lb-composer-mention-suggestions-list\">\n {mentions.map((mention) => {\n return (\n <ComposerPrimitive.SuggestionsListItem\n key={mention.id}\n className=\"lb-composer-suggestions-list-item lb-composer-mention-suggestion\"\n value={mention.id}\n >\n {mention.kind === \"user\" ? (\n <>\n <UserAvatar\n userId={mention.id}\n className=\"lb-composer-mention-suggestion-avatar\"\n />\n <User\n userId={mention.id}\n className=\"lb-composer-mention-suggestion-user\"\n />\n </>\n ) : mention.kind === \"group\" ? (\n <>\n <GroupAvatar\n groupId={mention.id}\n className=\"lb-composer-mention-suggestion-avatar\"\n icon={<UsersIcon />}\n />\n <Group\n groupId={mention.id}\n className=\"lb-composer-mention-suggestion-group\"\n >\n <GroupDescription\n groupId={mention.id}\n className=\"lb-composer-mention-suggestion-group-description\"\n />\n </Group>\n </>\n ) : (\n assertNever(mention, \"Unhandled mention kind\")\n )}\n </ComposerPrimitive.SuggestionsListItem>\n );\n })}\n </ComposerPrimitive.SuggestionsList>\n </ComposerPrimitive.Suggestions>\n ) : null;\n}\n\nfunction MarkToggle({\n mark,\n icon,\n shortcut,\n children,\n ...props\n}: MarkToggleProps) {\n const $ = useOverrides();\n const label = useMemo(() => {\n return $.COMPOSER_TOGGLE_MARK(mark);\n }, [$, mark]);\n\n return (\n <ShortcutTooltip\n content={label}\n shortcut={shortcut}\n sideOffset={FLOATING_ELEMENT_SIDE_OFFSET + 2}\n >\n <ComposerPrimitive.MarkToggle mark={mark} asChild {...props}>\n <Button aria-label={label} variant=\"toolbar\" icon={icon}>\n {children}\n </Button>\n </ComposerPrimitive.MarkToggle>\n </ShortcutTooltip>\n );\n}\n\ntype MarkToggles = {\n [K in ComposerBodyMark]: ComponentType<PropsWithChildren>;\n};\n\nconst markToggles: MarkToggles = {\n bold: () => <MarkToggle mark=\"bold\" shortcut=\"Mod-B\" icon={<BoldIcon />} />,\n italic: () => (\n <MarkToggle mark=\"italic\" shortcut=\"Mod-I\" icon={<ItalicIcon />} />\n ),\n strikethrough: () => (\n <MarkToggle\n mark=\"strikethrough\"\n shortcut=\"Mod-Shift-S\"\n icon={<StrikethroughIcon />}\n />\n ),\n code: () => <MarkToggle mark=\"code\" shortcut=\"Mod-E\" icon={<CodeIcon />} />,\n};\n\nconst markTogglesList = Object.entries(markToggles).map(([mark, Toggle]) => (\n <Toggle key={mark} />\n));\n\nfunction ComposerFloatingToolbar() {\n return (\n <ComposerPrimitive.FloatingToolbar className=\"lb-root lb-portal lb-elevation lb-composer-floating-toolbar\">\n {markTogglesList}\n </ComposerPrimitive.FloatingToolbar>\n );\n}\n\nfunction ComposerLink({ href, children }: ComposerEditorLinkProps) {\n return (\n <ComposerPrimitive.Link href={href} className=\"lb-composer-link\">\n {children}\n </ComposerPrimitive.Link>\n );\n}\n\ninterface ComposerAttachmentsProps extends ComponentPropsWithoutRef<\"div\"> {\n overrides?: Partial<GlobalOverrides & ComposerOverrides>;\n}\n\ninterface ComposerFileAttachmentProps extends ComponentPropsWithoutRef<\"div\"> {\n attachment: CommentMixedAttachment;\n overrides?: Partial<GlobalOverrides & ComposerOverrides>;\n}\n\nfunction ComposerFileAttachment({\n attachment,\n className,\n overrides,\n ...props\n}: ComposerFileAttachmentProps) {\n const { removeAttachment } = useComposer();\n const { roomId } = useComposerEditorContext();\n\n const handleDeleteClick = useCallback(() => {\n removeAttachment(attachment.id);\n }, [attachment.id, removeAttachment]);\n\n return (\n <FileAttachment\n className={cn(\"lb-composer-attachment\", className)}\n {...props}\n attachment={attachment}\n onDeleteClick={handleDeleteClick}\n preventFocusOnDelete\n overrides={overrides}\n roomId={roomId}\n />\n );\n}\n\nfunction ComposerAttachments({\n overrides,\n className,\n ...props\n}: ComposerAttachmentsProps) {\n const { attachments } = useComposer();\n\n if (attachments.length === 0) {\n return null;\n }\n\n return (\n <div className={cn(\"lb-composer-attachments\", className)} {...props}>\n <div className=\"lb-attachments\">\n {attachments.map((attachment) => {\n return (\n <ComposerFileAttachment\n key={attachment.id}\n attachment={attachment}\n overrides={overrides}\n />\n );\n })}\n </div>\n </div>\n );\n}\n\nconst editorRequiredComponents: ComposerEditorComponents = {\n Mention: ComposerMention,\n MentionSuggestions: ComposerMentionSuggestions,\n Link: ComposerLink,\n};\n\nfunction ComposerEditorContainer({\n showAttachments = true,\n showFormattingControls = true,\n showAttribution,\n defaultValue,\n isCollapsed,\n overrides,\n actions,\n autoFocus,\n disabled,\n hasResolveMentionSuggestions,\n onEmojiPickerOpenChange,\n onEmptyChange,\n onEditorClick,\n}: ComposerEditorContainerProps) {\n const { isEmpty } = useComposer();\n const { hasMaxAttachments } = useComposerAttachmentsContext();\n const $ = useOverrides(overrides);\n const components = useMemo(() => {\n return {\n ...editorRequiredComponents,\n FloatingToolbar: showFormattingControls\n ? ComposerFloatingToolbar\n : undefined,\n };\n }, [showFormattingControls]);\n\n const [isDraggingOver, dropAreaProps] = useComposerAttachmentsDropArea({\n disabled: disabled || !showAttachments || hasMaxAttachments,\n });\n\n useLayoutEffect(() => {\n onEmptyChange(isEmpty);\n }, [isEmpty, onEmptyChange]);\n\n const preventDefault = useCallback((event: SyntheticEvent) => {\n event.preventDefault();\n }, []);\n\n const stopPropagation = useCallback((event: SyntheticEvent) => {\n event.stopPropagation();\n }, []);\n\n return (\n <div className=\"lb-composer-editor-container\" {...dropAreaProps}>\n <ComposerPrimitive.Editor\n className=\"lb-composer-editor\"\n onClick={onEditorClick}\n placeholder={$.COMPOSER_PLACEHOLDER}\n defaultValue={defaultValue}\n autoFocus={autoFocus}\n components={components}\n disabled={disabled}\n dir={$.dir}\n />\n {showAttachments && <ComposerAttachments overrides={overrides} />}\n {(!isCollapsed || isDraggingOver) && (\n <div className=\"lb-composer-footer\">\n <div className=\"lb-composer-editor-actions\">\n {hasResolveMentionSuggestions && (\n <ComposerInsertMentionEditorAction\n label={$.COMPOSER_INSERT_MENTION}\n disabled={disabled}\n />\n )}\n <ComposerInsertEmojiEditorAction\n label={$.COMPOSER_INSERT_EMOJI}\n onPickerOpenChange={onEmojiPickerOpenChange}\n disabled={disabled}\n />\n {showAttachments && (\n <ComposerAttachFilesEditorAction\n label={$.COMPOSER_ATTACH_FILES}\n disabled={disabled}\n />\n )}\n </div>\n {showAttribution && <Attribution />}\n <div className=\"lb-composer-actions\">\n {actions ?? (\n <>\n <ShortcutTooltip content={$.COMPOSER_SEND} shortcut=\"Enter\">\n <ComposerPrimitive.Submit asChild>\n <Button\n onPointerDown={preventDefault}\n onClick={stopPropagation}\n className=\"lb-composer-action\"\n variant=\"primary\"\n aria-label={$.COMPOSER_SEND}\n icon={<SendIcon />}\n />\n </ComposerPrimitive.Submit>\n </ShortcutTooltip>\n </>\n )}\n </div>\n </div>\n )}\n {showAttachments && isDraggingOver && (\n <div className=\"lb-composer-attachments-drop-area\">\n <div className=\"lb-composer-attachments-drop-area-label\">\n <AttachmentIcon />\n {$.COMPOSER_ATTACH_FILES}\n </div>\n </div>\n )}\n </div>\n );\n}\n\nexport const ComposerRoomIdContext = createContext<string | null>(null);\n\n/**\n * Displays a composer to create comments.\n *\n * @example\n * <Composer />\n */\nexport const Composer = forwardRef(\n <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n {\n threadId,\n commentId,\n metadata,\n commentMetadata,\n defaultValue,\n defaultAttachments,\n onComposerSubmit,\n collapsed: controlledCollapsed,\n defaultCollapsed,\n onCollapsedChange: controlledOnCollapsedChange,\n overrides,\n actions,\n onBlur,\n className,\n onFocus,\n autoFocus,\n disabled,\n blurOnSubmit = true,\n showAttachments = true,\n showFormattingControls = true,\n showAttribution,\n roomId: _roomId,\n ...props\n }: ComposerProps<TM, CM>,\n forwardedRef: ForwardedRef<HTMLFormElement>\n ) => {\n const room = useRoom({ allowOutsideRoom: true });\n\n const roomId = _roomId !== undefined ? _roomId : room?.id;\n if (roomId === undefined) {\n throw new Error(\n \"Composer must be a descendant of RoomProvider component\"\n );\n }\n\n const createThread = useCreateRoomThread(roomId);\n const createComment = useCreateRoomComment(roomId);\n const editComment = useEditRoomComment(roomId);\n const { preventUnsavedComposerChanges } = useLiveblocksUiConfig();\n const hasResolveMentionSuggestions =\n useResolveMentionSuggestions() !== undefined;\n const isEmptyRef = useRef(true);\n const isEmojiPickerOpenRef = useRef(false);\n const $ = useOverrides(overrides);\n const [isCollapsed, onCollapsedChange] = useControllableState(\n defaultCollapsed ?? false,\n controlledCollapsed,\n controlledOnCollapsedChange\n );\n\n const canComment = useHasPermissionAccess(roomId, \"comments\", \"write\");\n\n const setEmptyRef = useCallback((isEmpty: boolean) => {\n isEmptyRef.current = isEmpty;\n }, []);\n\n const setEmojiPickerOpenRef = useCallback((isEmojiPickerOpen: boolean) => {\n isEmojiPickerOpenRef.current = isEmojiPickerOpen;\n }, []);\n\n const handleFocus = useCallback(\n (event: FocusEvent<HTMLFormElement>) => {\n onFocus?.(event);\n\n if (event.isDefaultPrevented()) {\n return;\n }\n\n if (isEmptyRef.current && canComment) {\n onCollapsedChange?.(false);\n }\n },\n [onCollapsedChange, onFocus, canComment]\n );\n\n const handleBlur = useCallback(\n (event: FocusEvent<HTMLFormElement>) => {\n onBlur?.(event);\n\n if (event.isDefaultPrevented()) {\n return;\n }\n\n const isOutside = !event.currentTarget.contains(\n event.relatedTarget ?? document.activeElement\n );\n\n if (isOutside && isEmptyRef.current && !isEmojiPickerOpenRef.current) {\n onCollapsedChange?.(true);\n }\n },\n [onBlur, onCollapsedChange]\n );\n\n const handleEditorClick = useCallback(\n (event: MouseEvent<HTMLDivElement>) => {\n event.stopPropagation();\n\n if (isEmptyRef.current && canComment) {\n onCollapsedChange?.(false);\n }\n },\n [onCollapsedChange, canComment]\n );\n\n const handleComposerSubmit = useCallback(\n (comment: ComposerSubmitComment, event: FormEvent<HTMLFormElement>) => {\n onComposerSubmit?.(comment, event);\n\n if (event.isDefaultPrevented()) {\n return;\n }\n\n event.stopPropagation();\n\n if (commentId && threadId) {\n editComment({\n commentId,\n threadId,\n body: comment.body,\n metadata: commentMetadata,\n attachments: comment.attachments,\n });\n } else if (threadId) {\n createComment({\n threadId,\n body: comment.body,\n metadata: commentMetadata as CM | undefined,\n attachments: comment.attachments,\n });\n } else {\n createThread({\n body: comment.body,\n metadata,\n commentMetadata: commentMetadata as CM | undefined,\n attachments: comment.attachments,\n });\n }\n },\n [\n commentId,\n createComment,\n createThread,\n editComment,\n commentMetadata,\n metadata,\n onComposerSubmit,\n threadId,\n ]\n );\n\n return (\n <TooltipProvider>\n <ComposerPrimitive.Form\n onComposerSubmit={handleComposerSubmit}\n className={cn(\"lb-root lb-composer lb-composer-form\", className)}\n dir={$.dir}\n {...props}\n ref={forwardedRef}\n data-collapsed={isCollapsed ? \"\" : undefined}\n onFocus={handleFocus}\n onBlur={handleBlur}\n disabled={disabled || !canComment}\n defaultAttachments={defaultAttachments}\n pasteFilesAsAttachments={showAttachments}\n preventUnsavedChanges={preventUnsavedComposerChanges}\n blurOnSubmit={blurOnSubmit}\n roomId={roomId}\n >\n <ComposerEditorContainer\n defaultValue={defaultValue}\n actions={actions}\n overrides={overrides}\n isCollapsed={isCollapsed}\n showAttachments={showAttachments}\n showAttribution={showAttribution}\n showFormattingControls={showFormattingControls}\n hasResolveMentionSuggestions={hasResolveMentionSuggestions}\n onEmptyChange={setEmptyRef}\n onEmojiPickerOpenChange={setEmojiPickerOpenRef}\n onEditorClick={handleEditorClick}\n autoFocus={autoFocus}\n disabled={disabled}\n />\n </ComposerPrimitive.Form>\n </TooltipProvider>\n );\n }\n) as <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n props: ComposerProps<TM, CM> & RefAttributes<HTMLFormElement>\n) => JSX.Element;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkQA;AAA2C;AACzC;AACA;AACA;AACA;AAEF;AACE;AAEA;AACE;AAAqB;AAGvB;AAAoB;AAEhB;AAEA;AACE;AACA;AAAc;AAChB;AACF;AACuB;AAGzB;AAEI;AAAC;AAAA;AACqD;AACrC;AACN;AACG;AACO;AACf;AAAA;AAIZ;AAEA;AAAyC;AACvC;AACA;AACA;AACA;AAEF;AACE;AAEA;AACE;AAAqB;AAGvB;AACE;AAAsB;AAGxB;AAIQ;AAAC;AAAA;AACqD;AACrC;AACN;AACG;AACK;AACb;AAAA;AAMhB;AAEA;AAAyC;AACvC;AACA;AACA;AAEF;AACE;AACE;AAAqB;AAGvB;AACE;AAAsB;AAGxB;AAGM;AAAC;AAAA;AACqD;AACrC;AACN;AACG;AACU;AAClB;AAAA;AAKd;AAEA;AACE;AAEI;AAAuD;AAC7B;AAGhC;AAEA;AACE;AAEA;AACE;AAAmB;AAAlB;AACW;AACiB;AAE3B;AAAuD;AAC3B;AAAA;AAAA;AAGlC;AAEO;AACL;AAAsB;AAElB;AAAyD;AAGzD;AAA0D;AAG1D;AAAoD;AAE1D;AAEA;AAAoC;AAEpC;AACE;AAIQ;AACE;AAAmB;AAAlB;AAEW;AACK;AAIX;AAAA;AAAC;AAAA;AACiB;AACN;AAAA;AACZ;AACA;AAAC;AAAA;AACiB;AACN;AAAA;AACZ;AAIA;AAAA;AAAC;AAAA;AACkB;AACP;AACO;AAAA;AACnB;AACA;AAAC;AAAA;AACkB;AACP;AAEV;AAAC;AAAA;AACkB;AACP;AAAA;AACZ;AAAA;AACF;AAG2C;AAAA;AAjClC;AAmCf;AAMZ;AAEA;AAAoB;AAClB;AACA;AACA;AACA;AAEF;AACE;AACA;AACE;AAAkC;AAGpC;AACE;AAAC;AAAA;AACU;AACT;AAC2C;AAM3C;AAAA;AAGN;AAMA;AAAiC;AAC0C;AAEN;AAGjE;AAAC;AAAA;AACM;AACI;AACgB;AAAA;AAC3B;AAGJ;AAEA;AAIA;AACE;AAKF;AAEA;AACE;AAKF;AAWA;AAAgC;AAC9B;AACA;AACA;AAEF;AACE;AACA;AAEA;AACE;AAA8B;AAGhC;AACE;AAAC;AAAA;AACkD;AAC7C;AACJ;AACe;AACK;AACpB;AACA;AAAA;AAGN;AAEA;AAA6B;AAC3B;AACA;AAEF;AACE;AAEA;AACE;AAAO;AAGT;AAIQ;AACE;AAAC;AAAA;AAEC;AACA;AAAA;AAFgB;AAGlB;AAMZ;AAEA;AAA2D;AAChD;AACW;AAEtB;AAEA;AAAiC;AACb;AACO;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEF;AACE;AACA;AACA;AACA;AACE;AAAO;AACF;AAGC;AACN;AAGF;AAAuE;AAC3B;AAG5C;AACE;AAAqB;AAGvB;AACE;AAAqB;AAGvB;AACE;AAAsB;AAGxB;AAEI;AAAA;AAAmB;AAAlB;AACW;AACD;AACM;AACf;AACA;AACA;AACA;AACO;AAAA;AACT;AAC+D;AAG3D;AACG;AACC;AAAC;AAAA;AACU;AACT;AAAA;AACF;AAEF;AAAC;AAAA;AACU;AACW;AACpB;AAAA;AACF;AAEE;AAAC;AAAA;AACU;AACT;AAAA;AACF;AAEJ;AACiC;AAMvB;AAAC;AAAA;AACgB;AACN;AACC;AACF;AACM;AACE;AAAA;AAM5B;AACF;AAKI;AAAgB;AACb;AAEP;AAIR;AAEa;AAQN;AAAiB;AAEpB;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACW;AACX;AACmB;AACnB;AACA;AACA;AACA;AACA;AACA;AACA;AACe;AACG;AACO;AACzB;AACQ;AACL;AAIL;AAEA;AACA;AACE;AAAU;AACR;AACF;AAGF;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAAyC;AACnB;AACpB;AACA;AAGF;AAEA;AACE;AAAqB;AAGvB;AACE;AAA+B;AAGjC;AAAoB;AAEhB;AAEA;AACE;AAAA;AAGF;AACE;AAAyB;AAC3B;AACF;AACuC;AAGzC;AAAmB;AAEf;AAEA;AACE;AAAA;AAGF;AAAuC;AACL;AAGlC;AACE;AAAwB;AAC1B;AACF;AAC0B;AAG5B;AAA0B;AAEtB;AAEA;AACE;AAAyB;AAC3B;AACF;AAC8B;AAGhC;AAA6B;AAEzB;AAEA;AACE;AAAA;AAGF;AAEA;AACE;AAAY;AACV;AACA;AACc;AACJ;AACW;AACtB;AAED;AAAc;AACZ;AACc;AACJ;AACW;AACtB;AAED;AAAa;AACG;AACd;AACA;AACqB;AACtB;AACH;AACF;AACA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF;AAGF;AAEI;AAAmB;AAAlB;AACmB;AAC6C;AACxD;AACH;AACC;AAC8B;AAC1B;AACD;AACe;AACvB;AACyB;AACF;AACvB;AACA;AAEA;AAAC;AAAA;AACC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe;AACU;AACV;AACf;AACA;AAAA;AACF;AAAA;AAEJ;AAGN;;;;"}
|
|
1
|
+
{"version":3,"file":"Composer.cjs","sources":["../../src/components/Composer.tsx"],"sourcesContent":["\"use client\";\n\nimport type {\n BaseMetadata,\n CommentAttachment,\n CommentMixedAttachment,\n DCM,\n DTM,\n GroupMentionData,\n Patchable,\n ThreadVisibility,\n} from \"@liveblocks/core\";\nimport { assertNever, MENTION_CHARACTER } from \"@liveblocks/core\";\nimport { useRoom } from \"@liveblocks/react\";\nimport {\n useCreateRoomComment,\n useCreateRoomThread,\n useEditRoomComment,\n useHasPermissionAccess,\n useLayoutEffect,\n useResolveMentionSuggestions,\n} from \"@liveblocks/react/_private\";\nimport type {\n ComponentPropsWithoutRef,\n ComponentType,\n FocusEvent,\n FormEvent,\n ForwardedRef,\n MouseEvent,\n PropsWithChildren,\n ReactNode,\n RefAttributes,\n SyntheticEvent,\n} from \"react\";\nimport { createContext, forwardRef, useCallback, useMemo, useRef } from \"react\";\n\nimport { useLiveblocksUiConfig } from \"../config\";\nimport { FLOATING_ELEMENT_SIDE_OFFSET } from \"../constants\";\nimport { UsersIcon } from \"../icons\";\nimport { AttachmentIcon } from \"../icons/Attachment\";\nimport { BoldIcon } from \"../icons/Bold\";\nimport { CodeIcon } from \"../icons/Code\";\nimport { EmojiIcon } from \"../icons/Emoji\";\nimport { ItalicIcon } from \"../icons/Italic\";\nimport { MentionIcon } from \"../icons/Mention\";\nimport { SendIcon } from \"../icons/Send\";\nimport { StrikethroughIcon } from \"../icons/Strikethrough\";\nimport type { ComposerOverrides, GlobalOverrides } from \"../overrides\";\nimport { useOverrides } from \"../overrides\";\nimport * as ComposerPrimitive from \"../primitives/Composer\";\nimport {\n useComposer,\n useComposerAttachmentsContext,\n useComposerEditorContext,\n} from \"../primitives/Composer/contexts\";\nimport type {\n ComposerEditorComponents,\n ComposerEditorLinkProps,\n ComposerEditorMentionProps,\n ComposerEditorMentionSuggestionsProps,\n ComposerEditorProps,\n ComposerFormProps,\n ComposerMarkToggleProps,\n ComposerSubmitComment,\n} from \"../primitives/Composer/types\";\nimport { useComposerAttachmentsDropArea } from \"../primitives/Composer/utils\";\nimport { commentsResourceForVisibility } from \"../shared\";\nimport type { ComposerBodyMark } from \"../types\";\nimport { cn } from \"../utils/cn\";\nimport { useControllableState } from \"../utils/use-controllable-state\";\nimport { useIsGroupMentionMember } from \"../utils/use-group-mention\";\nimport { GroupAvatar, UserAvatar } from \"./Avatar\";\nimport { FileAttachment } from \"./internal/Attachment\";\nimport { Attribution } from \"./internal/Attribution\";\nimport { Button } from \"./internal/Button\";\nimport type { EmojiPickerProps } from \"./internal/EmojiPicker\";\nimport { EmojiPicker, EmojiPickerTrigger } from \"./internal/EmojiPicker\";\nimport { Group } from \"./internal/Group\";\nimport { GroupDescription } from \"./internal/GroupDescription\";\nimport { ShortcutTooltip, Tooltip, TooltipProvider } from \"./internal/Tooltip\";\nimport { User } from \"./internal/User\";\n\ninterface EditorActionProps extends ComponentPropsWithoutRef<\"button\"> {\n label: string;\n tooltipLabel?: string;\n}\n\ninterface EmojiEditorActionProps extends EditorActionProps {\n onPickerOpenChange?: EmojiPickerProps[\"onOpenChange\"];\n}\n\ninterface MarkToggleProps extends ComposerMarkToggleProps {\n icon?: ReactNode;\n shortcut?: string;\n}\n\nexport type ComposerCreateThreadProps<\n TM extends BaseMetadata,\n CM extends BaseMetadata,\n> = {\n threadId?: never;\n\n commentId?: never;\n\n /**\n * The metadata of the thread to create.\n */\n metadata?: TM;\n\n /**\n * The visibility of the thread to create.\n */\n visibility?: ThreadVisibility;\n\n /**\n * The metadata of the comment to create.\n */\n commentMetadata?: CM;\n};\n\nexport type ComposerCreateCommentProps<CM extends BaseMetadata> = {\n /**\n * The ID of the thread to reply to.\n */\n threadId: string;\n\n commentId?: never;\n\n metadata?: never;\n\n /**\n * @internal\n * The visibility of the parent thread.\n */\n visibility?: ThreadVisibility;\n\n /**\n * The metadata of the comment to create.\n */\n commentMetadata?: CM;\n};\n\nexport type ComposerEditCommentProps<CM extends BaseMetadata> = {\n /**\n * The ID of the thread to edit a comment in.\n */\n threadId: string;\n\n /**\n * The ID of the comment to edit.\n */\n commentId: string;\n\n metadata?: never;\n\n /**\n * @internal\n * The visibility of the parent thread.\n */\n visibility?: ThreadVisibility;\n\n /**\n * The metadata of the comment to edit.\n */\n commentMetadata?: Patchable<CM>;\n};\n\nexport type ComposerProps<\n TM extends BaseMetadata = DTM,\n CM extends BaseMetadata = DCM,\n> = Omit<ComponentPropsWithoutRef<\"form\">, \"defaultValue\"> &\n (\n | ComposerCreateThreadProps<TM, CM>\n | ComposerCreateCommentProps<CM>\n | ComposerEditCommentProps<CM>\n ) & {\n /**\n * The event handler called when the composer is submitted.\n */\n onComposerSubmit?: (\n comment: ComposerSubmitComment,\n event: FormEvent<HTMLFormElement>\n ) => Promise<void> | void;\n\n /**\n * The composer's initial value.\n */\n defaultValue?: ComposerEditorProps[\"defaultValue\"];\n\n /**\n * The composer's initial attachments.\n */\n defaultAttachments?: CommentAttachment[];\n\n /**\n * Whether the composer is collapsed. Setting a value will make the composer controlled.\n */\n collapsed?: boolean;\n\n /**\n * The event handler called when the collapsed state of the composer changes.\n */\n onCollapsedChange?: (collapsed: boolean) => void;\n\n /**\n * Whether the composer is initially collapsed. Setting a value will make the composer uncontrolled.\n */\n defaultCollapsed?: boolean;\n\n /**\n * Whether to show and allow adding attachments.\n */\n showAttachments?: boolean;\n\n /**\n * Whether to show formatting controls (e.g. a floating toolbar with formatting toggles when selecting text)\n */\n showFormattingControls?: boolean;\n\n /**\n * Whether the composer is disabled.\n */\n disabled?: ComposerFormProps[\"disabled\"];\n\n /**\n * Whether to focus the composer on mount.\n */\n autoFocus?: ComposerEditorProps[\"autoFocus\"];\n\n /**\n * Whether to blur the composer editor when the composer is submitted.\n */\n blurOnSubmit?: boolean;\n\n /**\n * Override the component's strings.\n */\n overrides?: Partial<GlobalOverrides & ComposerOverrides>;\n\n /**\n * @internal\n */\n actions?: ReactNode;\n\n /**\n * @internal\n */\n showAttribution?: boolean;\n\n /**\n * @internal\n */\n roomId?: string;\n };\n\ninterface ComposerEditorContainerProps extends Pick<\n ComposerProps,\n | \"defaultValue\"\n | \"showAttachments\"\n | \"showFormattingControls\"\n | \"showAttribution\"\n | \"overrides\"\n | \"actions\"\n | \"autoFocus\"\n | \"disabled\"\n> {\n isCollapsed: boolean | undefined;\n onEmptyChange: (isEmpty: boolean) => void;\n hasResolveMentionSuggestions: boolean;\n onEmojiPickerOpenChange: (isOpen: boolean) => void;\n onEditorClick: (event: MouseEvent<HTMLDivElement>) => void;\n}\n\ninterface ComposerMentionProps extends ComposerEditorMentionProps {\n overrides?: ComposerProps[\"overrides\"];\n}\n\nfunction ComposerInsertMentionEditorAction({\n label,\n tooltipLabel,\n className,\n onClick,\n ...props\n}: EditorActionProps) {\n const { createMention } = useComposer();\n\n const preventDefault = useCallback((event: SyntheticEvent) => {\n event.preventDefault();\n }, []);\n\n const handleClick = useCallback(\n (event: MouseEvent<HTMLButtonElement>) => {\n onClick?.(event);\n\n if (!event.isDefaultPrevented()) {\n event.stopPropagation();\n createMention();\n }\n },\n [createMention, onClick]\n );\n\n return (\n <Tooltip content={tooltipLabel ?? label}>\n <Button\n className={cn(\"lb-composer-editor-action\", className)}\n onPointerDown={preventDefault}\n onClick={handleClick}\n aria-label={label}\n icon={<MentionIcon />}\n {...props}\n />\n </Tooltip>\n );\n}\n\nfunction ComposerInsertEmojiEditorAction({\n label,\n tooltipLabel,\n onPickerOpenChange,\n className,\n ...props\n}: EmojiEditorActionProps) {\n const { insertText } = useComposer();\n\n const preventDefault = useCallback((event: SyntheticEvent) => {\n event.preventDefault();\n }, []);\n\n const stopPropagation = useCallback((event: SyntheticEvent) => {\n event.stopPropagation();\n }, []);\n\n return (\n <EmojiPicker onEmojiSelect={insertText} onOpenChange={onPickerOpenChange}>\n <Tooltip content={tooltipLabel ?? label}>\n <EmojiPickerTrigger asChild>\n <Button\n className={cn(\"lb-composer-editor-action\", className)}\n onPointerDown={preventDefault}\n onClick={stopPropagation}\n aria-label={label}\n icon={<EmojiIcon />}\n {...props}\n />\n </EmojiPickerTrigger>\n </Tooltip>\n </EmojiPicker>\n );\n}\n\nfunction ComposerAttachFilesEditorAction({\n label,\n tooltipLabel,\n className,\n ...props\n}: EditorActionProps) {\n const preventDefault = useCallback((event: SyntheticEvent) => {\n event.preventDefault();\n }, []);\n\n const stopPropagation = useCallback((event: SyntheticEvent) => {\n event.stopPropagation();\n }, []);\n\n return (\n <Tooltip content={tooltipLabel ?? label}>\n <ComposerPrimitive.AttachFiles asChild>\n <Button\n className={cn(\"lb-composer-editor-action\", className)}\n onPointerDown={preventDefault}\n onClick={stopPropagation}\n aria-label={label}\n icon={<AttachmentIcon />}\n {...props}\n />\n </ComposerPrimitive.AttachFiles>\n </Tooltip>\n );\n}\n\nfunction ComposerUserMention({ mention }: ComposerMentionProps) {\n return (\n <ComposerPrimitive.Mention className=\"lb-mention lb-composer-mention\">\n <span className=\"lb-mention-symbol\">{MENTION_CHARACTER}</span>\n <User userId={mention.id} />\n </ComposerPrimitive.Mention>\n );\n}\n\nfunction ComposerGroupMention({ mention }: ComposerMentionProps) {\n const isMember = useIsGroupMentionMember(mention as GroupMentionData);\n\n return (\n <ComposerPrimitive.Mention\n className=\"lb-mention lb-composer-mention\"\n data-self={isMember ? \"\" : undefined}\n >\n <span className=\"lb-mention-symbol\">{MENTION_CHARACTER}</span>\n <Group groupId={mention.id} />\n </ComposerPrimitive.Mention>\n );\n}\n\nexport function ComposerMention({ mention, ...props }: ComposerMentionProps) {\n switch (mention.kind) {\n case \"user\":\n return <ComposerUserMention mention={mention} {...props} />;\n\n case \"group\":\n return <ComposerGroupMention mention={mention} {...props} />;\n\n default:\n return assertNever(mention, \"Unhandled mention kind\");\n }\n}\n\nfunction ComposerMentionSuggestions({\n mentions,\n}: ComposerEditorMentionSuggestionsProps) {\n return mentions.length > 0 ? (\n <ComposerPrimitive.Suggestions className=\"lb-root lb-portal lb-elevation lb-composer-suggestions lb-composer-mention-suggestions\">\n <ComposerPrimitive.SuggestionsList className=\"lb-composer-suggestions-list lb-composer-mention-suggestions-list\">\n {mentions.map((mention) => {\n return (\n <ComposerPrimitive.SuggestionsListItem\n key={mention.id}\n className=\"lb-composer-suggestions-list-item lb-composer-mention-suggestion\"\n value={mention.id}\n >\n {mention.kind === \"user\" ? (\n <>\n <UserAvatar\n userId={mention.id}\n className=\"lb-composer-mention-suggestion-avatar\"\n />\n <User\n userId={mention.id}\n className=\"lb-composer-mention-suggestion-user\"\n />\n </>\n ) : mention.kind === \"group\" ? (\n <>\n <GroupAvatar\n groupId={mention.id}\n className=\"lb-composer-mention-suggestion-avatar\"\n icon={<UsersIcon />}\n />\n <Group\n groupId={mention.id}\n className=\"lb-composer-mention-suggestion-group\"\n >\n <GroupDescription\n groupId={mention.id}\n className=\"lb-composer-mention-suggestion-group-description\"\n />\n </Group>\n </>\n ) : (\n assertNever(mention, \"Unhandled mention kind\")\n )}\n </ComposerPrimitive.SuggestionsListItem>\n );\n })}\n </ComposerPrimitive.SuggestionsList>\n </ComposerPrimitive.Suggestions>\n ) : null;\n}\n\nfunction MarkToggle({\n mark,\n icon,\n shortcut,\n children,\n ...props\n}: MarkToggleProps) {\n const $ = useOverrides();\n const label = useMemo(() => {\n return $.COMPOSER_TOGGLE_MARK(mark);\n }, [$, mark]);\n\n return (\n <ShortcutTooltip\n content={label}\n shortcut={shortcut}\n sideOffset={FLOATING_ELEMENT_SIDE_OFFSET + 2}\n >\n <ComposerPrimitive.MarkToggle mark={mark} asChild {...props}>\n <Button aria-label={label} variant=\"toolbar\" icon={icon}>\n {children}\n </Button>\n </ComposerPrimitive.MarkToggle>\n </ShortcutTooltip>\n );\n}\n\ntype MarkToggles = {\n [K in ComposerBodyMark]: ComponentType<PropsWithChildren>;\n};\n\nconst markToggles: MarkToggles = {\n bold: () => <MarkToggle mark=\"bold\" shortcut=\"Mod-B\" icon={<BoldIcon />} />,\n italic: () => (\n <MarkToggle mark=\"italic\" shortcut=\"Mod-I\" icon={<ItalicIcon />} />\n ),\n strikethrough: () => (\n <MarkToggle\n mark=\"strikethrough\"\n shortcut=\"Mod-Shift-S\"\n icon={<StrikethroughIcon />}\n />\n ),\n code: () => <MarkToggle mark=\"code\" shortcut=\"Mod-E\" icon={<CodeIcon />} />,\n};\n\nconst markTogglesList = Object.entries(markToggles).map(([mark, Toggle]) => (\n <Toggle key={mark} />\n));\n\nfunction ComposerFloatingToolbar() {\n return (\n <ComposerPrimitive.FloatingToolbar className=\"lb-root lb-portal lb-elevation lb-composer-floating-toolbar\">\n {markTogglesList}\n </ComposerPrimitive.FloatingToolbar>\n );\n}\n\nfunction ComposerLink({ href, children }: ComposerEditorLinkProps) {\n return (\n <ComposerPrimitive.Link href={href} className=\"lb-composer-link\">\n {children}\n </ComposerPrimitive.Link>\n );\n}\n\ninterface ComposerAttachmentsProps extends ComponentPropsWithoutRef<\"div\"> {\n overrides?: Partial<GlobalOverrides & ComposerOverrides>;\n}\n\ninterface ComposerFileAttachmentProps extends ComponentPropsWithoutRef<\"div\"> {\n attachment: CommentMixedAttachment;\n overrides?: Partial<GlobalOverrides & ComposerOverrides>;\n}\n\nfunction ComposerFileAttachment({\n attachment,\n className,\n overrides,\n ...props\n}: ComposerFileAttachmentProps) {\n const { removeAttachment } = useComposer();\n const { roomId } = useComposerEditorContext();\n\n const handleDeleteClick = useCallback(() => {\n removeAttachment(attachment.id);\n }, [attachment.id, removeAttachment]);\n\n return (\n <FileAttachment\n className={cn(\"lb-composer-attachment\", className)}\n {...props}\n attachment={attachment}\n onDeleteClick={handleDeleteClick}\n preventFocusOnDelete\n overrides={overrides}\n roomId={roomId}\n />\n );\n}\n\nfunction ComposerAttachments({\n overrides,\n className,\n ...props\n}: ComposerAttachmentsProps) {\n const { attachments } = useComposer();\n\n if (attachments.length === 0) {\n return null;\n }\n\n return (\n <div className={cn(\"lb-composer-attachments\", className)} {...props}>\n <div className=\"lb-attachments\">\n {attachments.map((attachment) => {\n return (\n <ComposerFileAttachment\n key={attachment.id}\n attachment={attachment}\n overrides={overrides}\n />\n );\n })}\n </div>\n </div>\n );\n}\n\nconst editorRequiredComponents: ComposerEditorComponents = {\n Mention: ComposerMention,\n MentionSuggestions: ComposerMentionSuggestions,\n Link: ComposerLink,\n};\n\nfunction ComposerEditorContainer({\n showAttachments = true,\n showFormattingControls = true,\n showAttribution,\n defaultValue,\n isCollapsed,\n overrides,\n actions,\n autoFocus,\n disabled,\n hasResolveMentionSuggestions,\n onEmojiPickerOpenChange,\n onEmptyChange,\n onEditorClick,\n}: ComposerEditorContainerProps) {\n const { isEmpty } = useComposer();\n const { hasMaxAttachments } = useComposerAttachmentsContext();\n const $ = useOverrides(overrides);\n const components = useMemo(() => {\n return {\n ...editorRequiredComponents,\n FloatingToolbar: showFormattingControls\n ? ComposerFloatingToolbar\n : undefined,\n };\n }, [showFormattingControls]);\n\n const [isDraggingOver, dropAreaProps] = useComposerAttachmentsDropArea({\n disabled: disabled || !showAttachments || hasMaxAttachments,\n });\n\n useLayoutEffect(() => {\n onEmptyChange(isEmpty);\n }, [isEmpty, onEmptyChange]);\n\n const preventDefault = useCallback((event: SyntheticEvent) => {\n event.preventDefault();\n }, []);\n\n const stopPropagation = useCallback((event: SyntheticEvent) => {\n event.stopPropagation();\n }, []);\n\n return (\n <div className=\"lb-composer-editor-container\" {...dropAreaProps}>\n <ComposerPrimitive.Editor\n className=\"lb-composer-editor\"\n onClick={onEditorClick}\n placeholder={$.COMPOSER_PLACEHOLDER}\n defaultValue={defaultValue}\n autoFocus={autoFocus}\n components={components}\n disabled={disabled}\n dir={$.dir}\n />\n {showAttachments && <ComposerAttachments overrides={overrides} />}\n {(!isCollapsed || isDraggingOver) && (\n <div className=\"lb-composer-footer\">\n <div className=\"lb-composer-editor-actions\">\n {hasResolveMentionSuggestions && (\n <ComposerInsertMentionEditorAction\n label={$.COMPOSER_INSERT_MENTION}\n disabled={disabled}\n />\n )}\n <ComposerInsertEmojiEditorAction\n label={$.COMPOSER_INSERT_EMOJI}\n onPickerOpenChange={onEmojiPickerOpenChange}\n disabled={disabled}\n />\n {showAttachments && (\n <ComposerAttachFilesEditorAction\n label={$.COMPOSER_ATTACH_FILES}\n disabled={disabled}\n />\n )}\n </div>\n {showAttribution && <Attribution />}\n <div className=\"lb-composer-actions\">\n {actions ?? (\n <>\n <ShortcutTooltip content={$.COMPOSER_SEND} shortcut=\"Enter\">\n <ComposerPrimitive.Submit asChild>\n <Button\n onPointerDown={preventDefault}\n onClick={stopPropagation}\n className=\"lb-composer-action\"\n variant=\"primary\"\n aria-label={$.COMPOSER_SEND}\n icon={<SendIcon />}\n />\n </ComposerPrimitive.Submit>\n </ShortcutTooltip>\n </>\n )}\n </div>\n </div>\n )}\n {showAttachments && isDraggingOver && (\n <div className=\"lb-composer-attachments-drop-area\">\n <div className=\"lb-composer-attachments-drop-area-label\">\n <AttachmentIcon />\n {$.COMPOSER_ATTACH_FILES}\n </div>\n </div>\n )}\n </div>\n );\n}\n\nexport const ComposerRoomIdContext = createContext<string | null>(null);\n\n/**\n * Displays a composer to create comments.\n *\n * @example\n * <Composer />\n */\nexport const Composer = forwardRef(\n <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n {\n threadId,\n commentId,\n metadata,\n visibility,\n commentMetadata,\n defaultValue,\n defaultAttachments,\n onComposerSubmit,\n collapsed: controlledCollapsed,\n defaultCollapsed,\n onCollapsedChange: controlledOnCollapsedChange,\n overrides,\n actions,\n onBlur,\n className,\n onFocus,\n autoFocus,\n disabled,\n blurOnSubmit = true,\n showAttachments = true,\n showFormattingControls = true,\n showAttribution,\n roomId: _roomId,\n ...props\n }: ComposerProps<TM, CM>,\n forwardedRef: ForwardedRef<HTMLFormElement>\n ) => {\n const room = useRoom({ allowOutsideRoom: true });\n\n const roomId = _roomId !== undefined ? _roomId : room?.id;\n if (roomId === undefined) {\n throw new Error(\n \"Composer must be a descendant of RoomProvider component\"\n );\n }\n\n const createThread = useCreateRoomThread(roomId);\n const createComment = useCreateRoomComment(roomId);\n const editComment = useEditRoomComment(roomId);\n const { preventUnsavedComposerChanges } = useLiveblocksUiConfig();\n const hasResolveMentionSuggestions =\n useResolveMentionSuggestions() !== undefined;\n const isEmptyRef = useRef(true);\n const isEmojiPickerOpenRef = useRef(false);\n const $ = useOverrides(overrides);\n const [isCollapsed, onCollapsedChange] = useControllableState(\n defaultCollapsed ?? false,\n controlledCollapsed,\n controlledOnCollapsedChange\n );\n const commentsResource =\n threadId === undefined\n ? commentsResourceForVisibility(visibility ?? \"public\")\n : commentsResourceForVisibility(visibility);\n const canComment = useHasPermissionAccess(\n roomId,\n commentsResource,\n \"write\"\n );\n\n const setEmptyRef = useCallback((isEmpty: boolean) => {\n isEmptyRef.current = isEmpty;\n }, []);\n\n const setEmojiPickerOpenRef = useCallback((isEmojiPickerOpen: boolean) => {\n isEmojiPickerOpenRef.current = isEmojiPickerOpen;\n }, []);\n\n const handleFocus = useCallback(\n (event: FocusEvent<HTMLFormElement>) => {\n onFocus?.(event);\n\n if (event.isDefaultPrevented()) {\n return;\n }\n\n if (isEmptyRef.current && canComment) {\n onCollapsedChange?.(false);\n }\n },\n [onCollapsedChange, onFocus, canComment]\n );\n\n const handleBlur = useCallback(\n (event: FocusEvent<HTMLFormElement>) => {\n onBlur?.(event);\n\n if (event.isDefaultPrevented()) {\n return;\n }\n\n const isOutside = !event.currentTarget.contains(\n event.relatedTarget ?? document.activeElement\n );\n\n if (isOutside && isEmptyRef.current && !isEmojiPickerOpenRef.current) {\n onCollapsedChange?.(true);\n }\n },\n [onBlur, onCollapsedChange]\n );\n\n const handleEditorClick = useCallback(\n (event: MouseEvent<HTMLDivElement>) => {\n event.stopPropagation();\n\n if (isEmptyRef.current && canComment) {\n onCollapsedChange?.(false);\n }\n },\n [onCollapsedChange, canComment]\n );\n\n const handleComposerSubmit = useCallback(\n (comment: ComposerSubmitComment, event: FormEvent<HTMLFormElement>) => {\n onComposerSubmit?.(comment, event);\n\n if (event.isDefaultPrevented()) {\n return;\n }\n\n event.stopPropagation();\n\n if (commentId && threadId) {\n editComment({\n commentId,\n threadId,\n body: comment.body,\n metadata: commentMetadata,\n attachments: comment.attachments,\n });\n } else if (threadId) {\n createComment({\n threadId,\n body: comment.body,\n metadata: commentMetadata as CM | undefined,\n attachments: comment.attachments,\n });\n } else {\n createThread({\n body: comment.body,\n metadata,\n visibility,\n commentMetadata: commentMetadata as CM | undefined,\n attachments: comment.attachments,\n });\n }\n },\n [\n commentId,\n createComment,\n createThread,\n editComment,\n commentMetadata,\n metadata,\n visibility,\n onComposerSubmit,\n threadId,\n ]\n );\n\n return (\n <TooltipProvider>\n <ComposerPrimitive.Form\n onComposerSubmit={handleComposerSubmit}\n className={cn(\"lb-root lb-composer lb-composer-form\", className)}\n dir={$.dir}\n {...props}\n ref={forwardedRef}\n data-collapsed={isCollapsed ? \"\" : undefined}\n onFocus={handleFocus}\n onBlur={handleBlur}\n disabled={disabled || !canComment}\n defaultAttachments={defaultAttachments}\n pasteFilesAsAttachments={showAttachments}\n preventUnsavedChanges={preventUnsavedComposerChanges}\n blurOnSubmit={blurOnSubmit}\n roomId={roomId}\n >\n <ComposerEditorContainer\n defaultValue={defaultValue}\n actions={actions}\n overrides={overrides}\n isCollapsed={isCollapsed}\n showAttachments={showAttachments}\n showAttribution={showAttribution}\n showFormattingControls={showFormattingControls}\n hasResolveMentionSuggestions={hasResolveMentionSuggestions}\n onEmptyChange={setEmptyRef}\n onEmojiPickerOpenChange={setEmojiPickerOpenRef}\n onEditorClick={handleEditorClick}\n autoFocus={autoFocus}\n disabled={disabled}\n />\n </ComposerPrimitive.Form>\n </TooltipProvider>\n );\n }\n) as <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n props: ComposerProps<TM, CM> & RefAttributes<HTMLFormElement>\n) => JSX.Element;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqRA;AAA2C;AACzC;AACA;AACA;AACA;AAEF;AACE;AAEA;AACE;AAAqB;AAGvB;AAAoB;AAEhB;AAEA;AACE;AACA;AAAc;AAChB;AACF;AACuB;AAGzB;AAEI;AAAC;AAAA;AACqD;AACrC;AACN;AACG;AACO;AACf;AAAA;AAIZ;AAEA;AAAyC;AACvC;AACA;AACA;AACA;AAEF;AACE;AAEA;AACE;AAAqB;AAGvB;AACE;AAAsB;AAGxB;AAIQ;AAAC;AAAA;AACqD;AACrC;AACN;AACG;AACK;AACb;AAAA;AAMhB;AAEA;AAAyC;AACvC;AACA;AACA;AAEF;AACE;AACE;AAAqB;AAGvB;AACE;AAAsB;AAGxB;AAGM;AAAC;AAAA;AACqD;AACrC;AACN;AACG;AACU;AAClB;AAAA;AAKd;AAEA;AACE;AAEI;AAAuD;AAC7B;AAGhC;AAEA;AACE;AAEA;AACE;AAAmB;AAAlB;AACW;AACiB;AAE3B;AAAuD;AAC3B;AAAA;AAAA;AAGlC;AAEO;AACL;AAAsB;AAElB;AAAyD;AAGzD;AAA0D;AAG1D;AAAoD;AAE1D;AAEA;AAAoC;AAEpC;AACE;AAIQ;AACE;AAAmB;AAAlB;AAEW;AACK;AAIX;AAAA;AAAC;AAAA;AACiB;AACN;AAAA;AACZ;AACA;AAAC;AAAA;AACiB;AACN;AAAA;AACZ;AAIA;AAAA;AAAC;AAAA;AACkB;AACP;AACO;AAAA;AACnB;AACA;AAAC;AAAA;AACkB;AACP;AAEV;AAAC;AAAA;AACkB;AACP;AAAA;AACZ;AAAA;AACF;AAG2C;AAAA;AAjClC;AAmCf;AAMZ;AAEA;AAAoB;AAClB;AACA;AACA;AACA;AAEF;AACE;AACA;AACE;AAAkC;AAGpC;AACE;AAAC;AAAA;AACU;AACT;AAC2C;AAM3C;AAAA;AAGN;AAMA;AAAiC;AAC0C;AAEN;AAGjE;AAAC;AAAA;AACM;AACI;AACgB;AAAA;AAC3B;AAGJ;AAEA;AAIA;AACE;AAKF;AAEA;AACE;AAKF;AAWA;AAAgC;AAC9B;AACA;AACA;AAEF;AACE;AACA;AAEA;AACE;AAA8B;AAGhC;AACE;AAAC;AAAA;AACkD;AAC7C;AACJ;AACe;AACK;AACpB;AACA;AAAA;AAGN;AAEA;AAA6B;AAC3B;AACA;AAEF;AACE;AAEA;AACE;AAAO;AAGT;AAIQ;AACE;AAAC;AAAA;AAEC;AACA;AAAA;AAFgB;AAGlB;AAMZ;AAEA;AAA2D;AAChD;AACW;AAEtB;AAEA;AAAiC;AACb;AACO;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEF;AACE;AACA;AACA;AACA;AACE;AAAO;AACF;AAGC;AACN;AAGF;AAAuE;AAC3B;AAG5C;AACE;AAAqB;AAGvB;AACE;AAAqB;AAGvB;AACE;AAAsB;AAGxB;AAEI;AAAA;AAAmB;AAAlB;AACW;AACD;AACM;AACf;AACA;AACA;AACA;AACO;AAAA;AACT;AAC+D;AAG3D;AACG;AACC;AAAC;AAAA;AACU;AACT;AAAA;AACF;AAEF;AAAC;AAAA;AACU;AACW;AACpB;AAAA;AACF;AAEE;AAAC;AAAA;AACU;AACT;AAAA;AACF;AAEJ;AACiC;AAMvB;AAAC;AAAA;AACgB;AACN;AACC;AACF;AACM;AACE;AAAA;AAM5B;AACF;AAKI;AAAgB;AACb;AAEP;AAIR;AAEa;AAQN;AAAiB;AAEpB;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACW;AACX;AACmB;AACnB;AACA;AACA;AACA;AACA;AACA;AACA;AACe;AACG;AACO;AACzB;AACQ;AACL;AAIL;AAEA;AACA;AACE;AAAU;AACR;AACF;AAGF;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAAyC;AACnB;AACpB;AACA;AAEF;AAIA;AAAmB;AACjB;AACA;AACA;AAGF;AACE;AAAqB;AAGvB;AACE;AAA+B;AAGjC;AAAoB;AAEhB;AAEA;AACE;AAAA;AAGF;AACE;AAAyB;AAC3B;AACF;AACuC;AAGzC;AAAmB;AAEf;AAEA;AACE;AAAA;AAGF;AAAuC;AACL;AAGlC;AACE;AAAwB;AAC1B;AACF;AAC0B;AAG5B;AAA0B;AAEtB;AAEA;AACE;AAAyB;AAC3B;AACF;AAC8B;AAGhC;AAA6B;AAEzB;AAEA;AACE;AAAA;AAGF;AAEA;AACE;AAAY;AACV;AACA;AACc;AACJ;AACW;AACtB;AAED;AAAc;AACZ;AACc;AACJ;AACW;AACtB;AAED;AAAa;AACG;AACd;AACA;AACA;AACqB;AACtB;AACH;AACF;AACA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF;AAGF;AAEI;AAAmB;AAAlB;AACmB;AAC6C;AACxD;AACH;AACC;AAC8B;AAC1B;AACD;AACe;AACvB;AACyB;AACF;AACvB;AACA;AAEA;AAAC;AAAA;AACC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe;AACU;AACV;AACf;AACA;AAAA;AACF;AAAA;AAEJ;AAGN;;;;"}
|
|
@@ -19,6 +19,7 @@ import { useOverrides } from '../overrides.js';
|
|
|
19
19
|
import { AttachFiles as ComposerAttachFiles, Mention as ComposerMention$1, Suggestions as ComposerSuggestions, SuggestionsList as ComposerSuggestionsList, SuggestionsListItem as ComposerSuggestionsListItem, MarkToggle as ComposerMarkToggle, FloatingToolbar as ComposerFloatingToolbar$1, Link as ComposerLink$1, Editor as ComposerEditor, Submit as ComposerSubmit, Form as ComposerForm } from '../primitives/Composer/index.js';
|
|
20
20
|
import { useComposer, useComposerEditorContext, useComposerAttachmentsContext } from '../primitives/Composer/contexts.js';
|
|
21
21
|
import { useComposerAttachmentsDropArea } from '../primitives/Composer/utils.js';
|
|
22
|
+
import { commentsResourceForVisibility } from '../shared.js';
|
|
22
23
|
import { cn } from '../utils/cn.js';
|
|
23
24
|
import { useControllableState } from '../utils/use-controllable-state.js';
|
|
24
25
|
import { useIsGroupMentionMember } from '../utils/use-group-mention.js';
|
|
@@ -391,6 +392,7 @@ const Composer = forwardRef(
|
|
|
391
392
|
threadId,
|
|
392
393
|
commentId,
|
|
393
394
|
metadata,
|
|
395
|
+
visibility,
|
|
394
396
|
commentMetadata,
|
|
395
397
|
defaultValue,
|
|
396
398
|
defaultAttachments,
|
|
@@ -432,7 +434,12 @@ const Composer = forwardRef(
|
|
|
432
434
|
controlledCollapsed,
|
|
433
435
|
controlledOnCollapsedChange
|
|
434
436
|
);
|
|
435
|
-
const
|
|
437
|
+
const commentsResource = threadId === void 0 ? commentsResourceForVisibility(visibility ?? "public") : commentsResourceForVisibility(visibility);
|
|
438
|
+
const canComment = useHasPermissionAccess(
|
|
439
|
+
roomId,
|
|
440
|
+
commentsResource,
|
|
441
|
+
"write"
|
|
442
|
+
);
|
|
436
443
|
const setEmptyRef = useCallback((isEmpty) => {
|
|
437
444
|
isEmptyRef.current = isEmpty;
|
|
438
445
|
}, []);
|
|
@@ -501,6 +508,7 @@ const Composer = forwardRef(
|
|
|
501
508
|
createThread({
|
|
502
509
|
body: comment.body,
|
|
503
510
|
metadata,
|
|
511
|
+
visibility,
|
|
504
512
|
commentMetadata,
|
|
505
513
|
attachments: comment.attachments
|
|
506
514
|
});
|
|
@@ -513,6 +521,7 @@ const Composer = forwardRef(
|
|
|
513
521
|
editComment,
|
|
514
522
|
commentMetadata,
|
|
515
523
|
metadata,
|
|
524
|
+
visibility,
|
|
516
525
|
onComposerSubmit,
|
|
517
526
|
threadId
|
|
518
527
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Composer.js","sources":["../../src/components/Composer.tsx"],"sourcesContent":["\"use client\";\n\nimport type {\n BaseMetadata,\n CommentAttachment,\n CommentMixedAttachment,\n DCM,\n DTM,\n GroupMentionData,\n Patchable,\n} from \"@liveblocks/core\";\nimport { assertNever, MENTION_CHARACTER } from \"@liveblocks/core\";\nimport { useRoom } from \"@liveblocks/react\";\nimport {\n useCreateRoomComment,\n useCreateRoomThread,\n useEditRoomComment,\n useHasPermissionAccess,\n useLayoutEffect,\n useResolveMentionSuggestions,\n} from \"@liveblocks/react/_private\";\nimport type {\n ComponentPropsWithoutRef,\n ComponentType,\n FocusEvent,\n FormEvent,\n ForwardedRef,\n MouseEvent,\n PropsWithChildren,\n ReactNode,\n RefAttributes,\n SyntheticEvent,\n} from \"react\";\nimport { createContext, forwardRef, useCallback, useMemo, useRef } from \"react\";\n\nimport { useLiveblocksUiConfig } from \"../config\";\nimport { FLOATING_ELEMENT_SIDE_OFFSET } from \"../constants\";\nimport { UsersIcon } from \"../icons\";\nimport { AttachmentIcon } from \"../icons/Attachment\";\nimport { BoldIcon } from \"../icons/Bold\";\nimport { CodeIcon } from \"../icons/Code\";\nimport { EmojiIcon } from \"../icons/Emoji\";\nimport { ItalicIcon } from \"../icons/Italic\";\nimport { MentionIcon } from \"../icons/Mention\";\nimport { SendIcon } from \"../icons/Send\";\nimport { StrikethroughIcon } from \"../icons/Strikethrough\";\nimport type { ComposerOverrides, GlobalOverrides } from \"../overrides\";\nimport { useOverrides } from \"../overrides\";\nimport * as ComposerPrimitive from \"../primitives/Composer\";\nimport {\n useComposer,\n useComposerAttachmentsContext,\n useComposerEditorContext,\n} from \"../primitives/Composer/contexts\";\nimport type {\n ComposerEditorComponents,\n ComposerEditorLinkProps,\n ComposerEditorMentionProps,\n ComposerEditorMentionSuggestionsProps,\n ComposerEditorProps,\n ComposerFormProps,\n ComposerMarkToggleProps,\n ComposerSubmitComment,\n} from \"../primitives/Composer/types\";\nimport { useComposerAttachmentsDropArea } from \"../primitives/Composer/utils\";\nimport type { ComposerBodyMark } from \"../types\";\nimport { cn } from \"../utils/cn\";\nimport { useControllableState } from \"../utils/use-controllable-state\";\nimport { useIsGroupMentionMember } from \"../utils/use-group-mention\";\nimport { GroupAvatar, UserAvatar } from \"./Avatar\";\nimport { FileAttachment } from \"./internal/Attachment\";\nimport { Attribution } from \"./internal/Attribution\";\nimport { Button } from \"./internal/Button\";\nimport type { EmojiPickerProps } from \"./internal/EmojiPicker\";\nimport { EmojiPicker, EmojiPickerTrigger } from \"./internal/EmojiPicker\";\nimport { Group } from \"./internal/Group\";\nimport { GroupDescription } from \"./internal/GroupDescription\";\nimport { ShortcutTooltip, Tooltip, TooltipProvider } from \"./internal/Tooltip\";\nimport { User } from \"./internal/User\";\n\ninterface EditorActionProps extends ComponentPropsWithoutRef<\"button\"> {\n label: string;\n tooltipLabel?: string;\n}\n\ninterface EmojiEditorActionProps extends EditorActionProps {\n onPickerOpenChange?: EmojiPickerProps[\"onOpenChange\"];\n}\n\ninterface MarkToggleProps extends ComposerMarkToggleProps {\n icon?: ReactNode;\n shortcut?: string;\n}\n\nexport type ComposerCreateThreadProps<\n TM extends BaseMetadata,\n CM extends BaseMetadata,\n> = {\n threadId?: never;\n\n commentId?: never;\n\n /**\n * The metadata of the thread to create.\n */\n metadata?: TM;\n\n /**\n * The metadata of the comment to create.\n */\n commentMetadata?: CM;\n};\n\nexport type ComposerCreateCommentProps<CM extends BaseMetadata> = {\n /**\n * The ID of the thread to reply to.\n */\n threadId: string;\n\n commentId?: never;\n\n metadata?: never;\n\n /**\n * The metadata of the comment to create.\n */\n commentMetadata?: CM;\n};\n\nexport type ComposerEditCommentProps<CM extends BaseMetadata> = {\n /**\n * The ID of the thread to edit a comment in.\n */\n threadId: string;\n\n /**\n * The ID of the comment to edit.\n */\n commentId: string;\n\n metadata?: never;\n\n /**\n * The metadata of the comment to edit.\n */\n commentMetadata?: Patchable<CM>;\n};\n\nexport type ComposerProps<\n TM extends BaseMetadata = DTM,\n CM extends BaseMetadata = DCM,\n> = Omit<ComponentPropsWithoutRef<\"form\">, \"defaultValue\"> &\n (\n | ComposerCreateThreadProps<TM, CM>\n | ComposerCreateCommentProps<CM>\n | ComposerEditCommentProps<CM>\n ) & {\n /**\n * The event handler called when the composer is submitted.\n */\n onComposerSubmit?: (\n comment: ComposerSubmitComment,\n event: FormEvent<HTMLFormElement>\n ) => Promise<void> | void;\n\n /**\n * The composer's initial value.\n */\n defaultValue?: ComposerEditorProps[\"defaultValue\"];\n\n /**\n * The composer's initial attachments.\n */\n defaultAttachments?: CommentAttachment[];\n\n /**\n * Whether the composer is collapsed. Setting a value will make the composer controlled.\n */\n collapsed?: boolean;\n\n /**\n * The event handler called when the collapsed state of the composer changes.\n */\n onCollapsedChange?: (collapsed: boolean) => void;\n\n /**\n * Whether the composer is initially collapsed. Setting a value will make the composer uncontrolled.\n */\n defaultCollapsed?: boolean;\n\n /**\n * Whether to show and allow adding attachments.\n */\n showAttachments?: boolean;\n\n /**\n * Whether to show formatting controls (e.g. a floating toolbar with formatting toggles when selecting text)\n */\n showFormattingControls?: boolean;\n\n /**\n * Whether the composer is disabled.\n */\n disabled?: ComposerFormProps[\"disabled\"];\n\n /**\n * Whether to focus the composer on mount.\n */\n autoFocus?: ComposerEditorProps[\"autoFocus\"];\n\n /**\n * Whether to blur the composer editor when the composer is submitted.\n */\n blurOnSubmit?: boolean;\n\n /**\n * Override the component's strings.\n */\n overrides?: Partial<GlobalOverrides & ComposerOverrides>;\n\n /**\n * @internal\n */\n actions?: ReactNode;\n\n /**\n * @internal\n */\n showAttribution?: boolean;\n\n /**\n * @internal\n */\n roomId?: string;\n };\n\ninterface ComposerEditorContainerProps extends Pick<\n ComposerProps,\n | \"defaultValue\"\n | \"showAttachments\"\n | \"showFormattingControls\"\n | \"showAttribution\"\n | \"overrides\"\n | \"actions\"\n | \"autoFocus\"\n | \"disabled\"\n> {\n isCollapsed: boolean | undefined;\n onEmptyChange: (isEmpty: boolean) => void;\n hasResolveMentionSuggestions: boolean;\n onEmojiPickerOpenChange: (isOpen: boolean) => void;\n onEditorClick: (event: MouseEvent<HTMLDivElement>) => void;\n}\n\ninterface ComposerMentionProps extends ComposerEditorMentionProps {\n overrides?: ComposerProps[\"overrides\"];\n}\n\nfunction ComposerInsertMentionEditorAction({\n label,\n tooltipLabel,\n className,\n onClick,\n ...props\n}: EditorActionProps) {\n const { createMention } = useComposer();\n\n const preventDefault = useCallback((event: SyntheticEvent) => {\n event.preventDefault();\n }, []);\n\n const handleClick = useCallback(\n (event: MouseEvent<HTMLButtonElement>) => {\n onClick?.(event);\n\n if (!event.isDefaultPrevented()) {\n event.stopPropagation();\n createMention();\n }\n },\n [createMention, onClick]\n );\n\n return (\n <Tooltip content={tooltipLabel ?? label}>\n <Button\n className={cn(\"lb-composer-editor-action\", className)}\n onPointerDown={preventDefault}\n onClick={handleClick}\n aria-label={label}\n icon={<MentionIcon />}\n {...props}\n />\n </Tooltip>\n );\n}\n\nfunction ComposerInsertEmojiEditorAction({\n label,\n tooltipLabel,\n onPickerOpenChange,\n className,\n ...props\n}: EmojiEditorActionProps) {\n const { insertText } = useComposer();\n\n const preventDefault = useCallback((event: SyntheticEvent) => {\n event.preventDefault();\n }, []);\n\n const stopPropagation = useCallback((event: SyntheticEvent) => {\n event.stopPropagation();\n }, []);\n\n return (\n <EmojiPicker onEmojiSelect={insertText} onOpenChange={onPickerOpenChange}>\n <Tooltip content={tooltipLabel ?? label}>\n <EmojiPickerTrigger asChild>\n <Button\n className={cn(\"lb-composer-editor-action\", className)}\n onPointerDown={preventDefault}\n onClick={stopPropagation}\n aria-label={label}\n icon={<EmojiIcon />}\n {...props}\n />\n </EmojiPickerTrigger>\n </Tooltip>\n </EmojiPicker>\n );\n}\n\nfunction ComposerAttachFilesEditorAction({\n label,\n tooltipLabel,\n className,\n ...props\n}: EditorActionProps) {\n const preventDefault = useCallback((event: SyntheticEvent) => {\n event.preventDefault();\n }, []);\n\n const stopPropagation = useCallback((event: SyntheticEvent) => {\n event.stopPropagation();\n }, []);\n\n return (\n <Tooltip content={tooltipLabel ?? label}>\n <ComposerPrimitive.AttachFiles asChild>\n <Button\n className={cn(\"lb-composer-editor-action\", className)}\n onPointerDown={preventDefault}\n onClick={stopPropagation}\n aria-label={label}\n icon={<AttachmentIcon />}\n {...props}\n />\n </ComposerPrimitive.AttachFiles>\n </Tooltip>\n );\n}\n\nfunction ComposerUserMention({ mention }: ComposerMentionProps) {\n return (\n <ComposerPrimitive.Mention className=\"lb-mention lb-composer-mention\">\n <span className=\"lb-mention-symbol\">{MENTION_CHARACTER}</span>\n <User userId={mention.id} />\n </ComposerPrimitive.Mention>\n );\n}\n\nfunction ComposerGroupMention({ mention }: ComposerMentionProps) {\n const isMember = useIsGroupMentionMember(mention as GroupMentionData);\n\n return (\n <ComposerPrimitive.Mention\n className=\"lb-mention lb-composer-mention\"\n data-self={isMember ? \"\" : undefined}\n >\n <span className=\"lb-mention-symbol\">{MENTION_CHARACTER}</span>\n <Group groupId={mention.id} />\n </ComposerPrimitive.Mention>\n );\n}\n\nexport function ComposerMention({ mention, ...props }: ComposerMentionProps) {\n switch (mention.kind) {\n case \"user\":\n return <ComposerUserMention mention={mention} {...props} />;\n\n case \"group\":\n return <ComposerGroupMention mention={mention} {...props} />;\n\n default:\n return assertNever(mention, \"Unhandled mention kind\");\n }\n}\n\nfunction ComposerMentionSuggestions({\n mentions,\n}: ComposerEditorMentionSuggestionsProps) {\n return mentions.length > 0 ? (\n <ComposerPrimitive.Suggestions className=\"lb-root lb-portal lb-elevation lb-composer-suggestions lb-composer-mention-suggestions\">\n <ComposerPrimitive.SuggestionsList className=\"lb-composer-suggestions-list lb-composer-mention-suggestions-list\">\n {mentions.map((mention) => {\n return (\n <ComposerPrimitive.SuggestionsListItem\n key={mention.id}\n className=\"lb-composer-suggestions-list-item lb-composer-mention-suggestion\"\n value={mention.id}\n >\n {mention.kind === \"user\" ? (\n <>\n <UserAvatar\n userId={mention.id}\n className=\"lb-composer-mention-suggestion-avatar\"\n />\n <User\n userId={mention.id}\n className=\"lb-composer-mention-suggestion-user\"\n />\n </>\n ) : mention.kind === \"group\" ? (\n <>\n <GroupAvatar\n groupId={mention.id}\n className=\"lb-composer-mention-suggestion-avatar\"\n icon={<UsersIcon />}\n />\n <Group\n groupId={mention.id}\n className=\"lb-composer-mention-suggestion-group\"\n >\n <GroupDescription\n groupId={mention.id}\n className=\"lb-composer-mention-suggestion-group-description\"\n />\n </Group>\n </>\n ) : (\n assertNever(mention, \"Unhandled mention kind\")\n )}\n </ComposerPrimitive.SuggestionsListItem>\n );\n })}\n </ComposerPrimitive.SuggestionsList>\n </ComposerPrimitive.Suggestions>\n ) : null;\n}\n\nfunction MarkToggle({\n mark,\n icon,\n shortcut,\n children,\n ...props\n}: MarkToggleProps) {\n const $ = useOverrides();\n const label = useMemo(() => {\n return $.COMPOSER_TOGGLE_MARK(mark);\n }, [$, mark]);\n\n return (\n <ShortcutTooltip\n content={label}\n shortcut={shortcut}\n sideOffset={FLOATING_ELEMENT_SIDE_OFFSET + 2}\n >\n <ComposerPrimitive.MarkToggle mark={mark} asChild {...props}>\n <Button aria-label={label} variant=\"toolbar\" icon={icon}>\n {children}\n </Button>\n </ComposerPrimitive.MarkToggle>\n </ShortcutTooltip>\n );\n}\n\ntype MarkToggles = {\n [K in ComposerBodyMark]: ComponentType<PropsWithChildren>;\n};\n\nconst markToggles: MarkToggles = {\n bold: () => <MarkToggle mark=\"bold\" shortcut=\"Mod-B\" icon={<BoldIcon />} />,\n italic: () => (\n <MarkToggle mark=\"italic\" shortcut=\"Mod-I\" icon={<ItalicIcon />} />\n ),\n strikethrough: () => (\n <MarkToggle\n mark=\"strikethrough\"\n shortcut=\"Mod-Shift-S\"\n icon={<StrikethroughIcon />}\n />\n ),\n code: () => <MarkToggle mark=\"code\" shortcut=\"Mod-E\" icon={<CodeIcon />} />,\n};\n\nconst markTogglesList = Object.entries(markToggles).map(([mark, Toggle]) => (\n <Toggle key={mark} />\n));\n\nfunction ComposerFloatingToolbar() {\n return (\n <ComposerPrimitive.FloatingToolbar className=\"lb-root lb-portal lb-elevation lb-composer-floating-toolbar\">\n {markTogglesList}\n </ComposerPrimitive.FloatingToolbar>\n );\n}\n\nfunction ComposerLink({ href, children }: ComposerEditorLinkProps) {\n return (\n <ComposerPrimitive.Link href={href} className=\"lb-composer-link\">\n {children}\n </ComposerPrimitive.Link>\n );\n}\n\ninterface ComposerAttachmentsProps extends ComponentPropsWithoutRef<\"div\"> {\n overrides?: Partial<GlobalOverrides & ComposerOverrides>;\n}\n\ninterface ComposerFileAttachmentProps extends ComponentPropsWithoutRef<\"div\"> {\n attachment: CommentMixedAttachment;\n overrides?: Partial<GlobalOverrides & ComposerOverrides>;\n}\n\nfunction ComposerFileAttachment({\n attachment,\n className,\n overrides,\n ...props\n}: ComposerFileAttachmentProps) {\n const { removeAttachment } = useComposer();\n const { roomId } = useComposerEditorContext();\n\n const handleDeleteClick = useCallback(() => {\n removeAttachment(attachment.id);\n }, [attachment.id, removeAttachment]);\n\n return (\n <FileAttachment\n className={cn(\"lb-composer-attachment\", className)}\n {...props}\n attachment={attachment}\n onDeleteClick={handleDeleteClick}\n preventFocusOnDelete\n overrides={overrides}\n roomId={roomId}\n />\n );\n}\n\nfunction ComposerAttachments({\n overrides,\n className,\n ...props\n}: ComposerAttachmentsProps) {\n const { attachments } = useComposer();\n\n if (attachments.length === 0) {\n return null;\n }\n\n return (\n <div className={cn(\"lb-composer-attachments\", className)} {...props}>\n <div className=\"lb-attachments\">\n {attachments.map((attachment) => {\n return (\n <ComposerFileAttachment\n key={attachment.id}\n attachment={attachment}\n overrides={overrides}\n />\n );\n })}\n </div>\n </div>\n );\n}\n\nconst editorRequiredComponents: ComposerEditorComponents = {\n Mention: ComposerMention,\n MentionSuggestions: ComposerMentionSuggestions,\n Link: ComposerLink,\n};\n\nfunction ComposerEditorContainer({\n showAttachments = true,\n showFormattingControls = true,\n showAttribution,\n defaultValue,\n isCollapsed,\n overrides,\n actions,\n autoFocus,\n disabled,\n hasResolveMentionSuggestions,\n onEmojiPickerOpenChange,\n onEmptyChange,\n onEditorClick,\n}: ComposerEditorContainerProps) {\n const { isEmpty } = useComposer();\n const { hasMaxAttachments } = useComposerAttachmentsContext();\n const $ = useOverrides(overrides);\n const components = useMemo(() => {\n return {\n ...editorRequiredComponents,\n FloatingToolbar: showFormattingControls\n ? ComposerFloatingToolbar\n : undefined,\n };\n }, [showFormattingControls]);\n\n const [isDraggingOver, dropAreaProps] = useComposerAttachmentsDropArea({\n disabled: disabled || !showAttachments || hasMaxAttachments,\n });\n\n useLayoutEffect(() => {\n onEmptyChange(isEmpty);\n }, [isEmpty, onEmptyChange]);\n\n const preventDefault = useCallback((event: SyntheticEvent) => {\n event.preventDefault();\n }, []);\n\n const stopPropagation = useCallback((event: SyntheticEvent) => {\n event.stopPropagation();\n }, []);\n\n return (\n <div className=\"lb-composer-editor-container\" {...dropAreaProps}>\n <ComposerPrimitive.Editor\n className=\"lb-composer-editor\"\n onClick={onEditorClick}\n placeholder={$.COMPOSER_PLACEHOLDER}\n defaultValue={defaultValue}\n autoFocus={autoFocus}\n components={components}\n disabled={disabled}\n dir={$.dir}\n />\n {showAttachments && <ComposerAttachments overrides={overrides} />}\n {(!isCollapsed || isDraggingOver) && (\n <div className=\"lb-composer-footer\">\n <div className=\"lb-composer-editor-actions\">\n {hasResolveMentionSuggestions && (\n <ComposerInsertMentionEditorAction\n label={$.COMPOSER_INSERT_MENTION}\n disabled={disabled}\n />\n )}\n <ComposerInsertEmojiEditorAction\n label={$.COMPOSER_INSERT_EMOJI}\n onPickerOpenChange={onEmojiPickerOpenChange}\n disabled={disabled}\n />\n {showAttachments && (\n <ComposerAttachFilesEditorAction\n label={$.COMPOSER_ATTACH_FILES}\n disabled={disabled}\n />\n )}\n </div>\n {showAttribution && <Attribution />}\n <div className=\"lb-composer-actions\">\n {actions ?? (\n <>\n <ShortcutTooltip content={$.COMPOSER_SEND} shortcut=\"Enter\">\n <ComposerPrimitive.Submit asChild>\n <Button\n onPointerDown={preventDefault}\n onClick={stopPropagation}\n className=\"lb-composer-action\"\n variant=\"primary\"\n aria-label={$.COMPOSER_SEND}\n icon={<SendIcon />}\n />\n </ComposerPrimitive.Submit>\n </ShortcutTooltip>\n </>\n )}\n </div>\n </div>\n )}\n {showAttachments && isDraggingOver && (\n <div className=\"lb-composer-attachments-drop-area\">\n <div className=\"lb-composer-attachments-drop-area-label\">\n <AttachmentIcon />\n {$.COMPOSER_ATTACH_FILES}\n </div>\n </div>\n )}\n </div>\n );\n}\n\nexport const ComposerRoomIdContext = createContext<string | null>(null);\n\n/**\n * Displays a composer to create comments.\n *\n * @example\n * <Composer />\n */\nexport const Composer = forwardRef(\n <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n {\n threadId,\n commentId,\n metadata,\n commentMetadata,\n defaultValue,\n defaultAttachments,\n onComposerSubmit,\n collapsed: controlledCollapsed,\n defaultCollapsed,\n onCollapsedChange: controlledOnCollapsedChange,\n overrides,\n actions,\n onBlur,\n className,\n onFocus,\n autoFocus,\n disabled,\n blurOnSubmit = true,\n showAttachments = true,\n showFormattingControls = true,\n showAttribution,\n roomId: _roomId,\n ...props\n }: ComposerProps<TM, CM>,\n forwardedRef: ForwardedRef<HTMLFormElement>\n ) => {\n const room = useRoom({ allowOutsideRoom: true });\n\n const roomId = _roomId !== undefined ? _roomId : room?.id;\n if (roomId === undefined) {\n throw new Error(\n \"Composer must be a descendant of RoomProvider component\"\n );\n }\n\n const createThread = useCreateRoomThread(roomId);\n const createComment = useCreateRoomComment(roomId);\n const editComment = useEditRoomComment(roomId);\n const { preventUnsavedComposerChanges } = useLiveblocksUiConfig();\n const hasResolveMentionSuggestions =\n useResolveMentionSuggestions() !== undefined;\n const isEmptyRef = useRef(true);\n const isEmojiPickerOpenRef = useRef(false);\n const $ = useOverrides(overrides);\n const [isCollapsed, onCollapsedChange] = useControllableState(\n defaultCollapsed ?? false,\n controlledCollapsed,\n controlledOnCollapsedChange\n );\n\n const canComment = useHasPermissionAccess(roomId, \"comments\", \"write\");\n\n const setEmptyRef = useCallback((isEmpty: boolean) => {\n isEmptyRef.current = isEmpty;\n }, []);\n\n const setEmojiPickerOpenRef = useCallback((isEmojiPickerOpen: boolean) => {\n isEmojiPickerOpenRef.current = isEmojiPickerOpen;\n }, []);\n\n const handleFocus = useCallback(\n (event: FocusEvent<HTMLFormElement>) => {\n onFocus?.(event);\n\n if (event.isDefaultPrevented()) {\n return;\n }\n\n if (isEmptyRef.current && canComment) {\n onCollapsedChange?.(false);\n }\n },\n [onCollapsedChange, onFocus, canComment]\n );\n\n const handleBlur = useCallback(\n (event: FocusEvent<HTMLFormElement>) => {\n onBlur?.(event);\n\n if (event.isDefaultPrevented()) {\n return;\n }\n\n const isOutside = !event.currentTarget.contains(\n event.relatedTarget ?? document.activeElement\n );\n\n if (isOutside && isEmptyRef.current && !isEmojiPickerOpenRef.current) {\n onCollapsedChange?.(true);\n }\n },\n [onBlur, onCollapsedChange]\n );\n\n const handleEditorClick = useCallback(\n (event: MouseEvent<HTMLDivElement>) => {\n event.stopPropagation();\n\n if (isEmptyRef.current && canComment) {\n onCollapsedChange?.(false);\n }\n },\n [onCollapsedChange, canComment]\n );\n\n const handleComposerSubmit = useCallback(\n (comment: ComposerSubmitComment, event: FormEvent<HTMLFormElement>) => {\n onComposerSubmit?.(comment, event);\n\n if (event.isDefaultPrevented()) {\n return;\n }\n\n event.stopPropagation();\n\n if (commentId && threadId) {\n editComment({\n commentId,\n threadId,\n body: comment.body,\n metadata: commentMetadata,\n attachments: comment.attachments,\n });\n } else if (threadId) {\n createComment({\n threadId,\n body: comment.body,\n metadata: commentMetadata as CM | undefined,\n attachments: comment.attachments,\n });\n } else {\n createThread({\n body: comment.body,\n metadata,\n commentMetadata: commentMetadata as CM | undefined,\n attachments: comment.attachments,\n });\n }\n },\n [\n commentId,\n createComment,\n createThread,\n editComment,\n commentMetadata,\n metadata,\n onComposerSubmit,\n threadId,\n ]\n );\n\n return (\n <TooltipProvider>\n <ComposerPrimitive.Form\n onComposerSubmit={handleComposerSubmit}\n className={cn(\"lb-root lb-composer lb-composer-form\", className)}\n dir={$.dir}\n {...props}\n ref={forwardedRef}\n data-collapsed={isCollapsed ? \"\" : undefined}\n onFocus={handleFocus}\n onBlur={handleBlur}\n disabled={disabled || !canComment}\n defaultAttachments={defaultAttachments}\n pasteFilesAsAttachments={showAttachments}\n preventUnsavedChanges={preventUnsavedComposerChanges}\n blurOnSubmit={blurOnSubmit}\n roomId={roomId}\n >\n <ComposerEditorContainer\n defaultValue={defaultValue}\n actions={actions}\n overrides={overrides}\n isCollapsed={isCollapsed}\n showAttachments={showAttachments}\n showAttribution={showAttribution}\n showFormattingControls={showFormattingControls}\n hasResolveMentionSuggestions={hasResolveMentionSuggestions}\n onEmptyChange={setEmptyRef}\n onEmojiPickerOpenChange={setEmojiPickerOpenRef}\n onEditorClick={handleEditorClick}\n autoFocus={autoFocus}\n disabled={disabled}\n />\n </ComposerPrimitive.Form>\n </TooltipProvider>\n );\n }\n) as <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n props: ComposerProps<TM, CM> & RefAttributes<HTMLFormElement>\n) => JSX.Element;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkQA;AAA2C;AACzC;AACA;AACA;AACA;AAEF;AACE;AAEA;AACE;AAAqB;AAGvB;AAAoB;AAEhB;AAEA;AACE;AACA;AAAc;AAChB;AACF;AACuB;AAGzB;AAEI;AAAC;AAAA;AACqD;AACrC;AACN;AACG;AACO;AACf;AAAA;AAIZ;AAEA;AAAyC;AACvC;AACA;AACA;AACA;AAEF;AACE;AAEA;AACE;AAAqB;AAGvB;AACE;AAAsB;AAGxB;AAIQ;AAAC;AAAA;AACqD;AACrC;AACN;AACG;AACK;AACb;AAAA;AAMhB;AAEA;AAAyC;AACvC;AACA;AACA;AAEF;AACE;AACE;AAAqB;AAGvB;AACE;AAAsB;AAGxB;AAGM;AAAC;AAAA;AACqD;AACrC;AACN;AACG;AACU;AAClB;AAAA;AAKd;AAEA;AACE;AAEI;AAAuD;AAC7B;AAGhC;AAEA;AACE;AAEA;AACE;AAAmB;AAAlB;AACW;AACiB;AAE3B;AAAuD;AAC3B;AAAA;AAAA;AAGlC;AAEO;AACL;AAAsB;AAElB;AAAyD;AAGzD;AAA0D;AAG1D;AAAoD;AAE1D;AAEA;AAAoC;AAEpC;AACE;AAIQ;AACE;AAAmB;AAAlB;AAEW;AACK;AAIX;AAAA;AAAC;AAAA;AACiB;AACN;AAAA;AACZ;AACA;AAAC;AAAA;AACiB;AACN;AAAA;AACZ;AAIA;AAAA;AAAC;AAAA;AACkB;AACP;AACO;AAAA;AACnB;AACA;AAAC;AAAA;AACkB;AACP;AAEV;AAAC;AAAA;AACkB;AACP;AAAA;AACZ;AAAA;AACF;AAG2C;AAAA;AAjClC;AAmCf;AAMZ;AAEA;AAAoB;AAClB;AACA;AACA;AACA;AAEF;AACE;AACA;AACE;AAAkC;AAGpC;AACE;AAAC;AAAA;AACU;AACT;AAC2C;AAM3C;AAAA;AAGN;AAMA;AAAiC;AAC0C;AAEN;AAGjE;AAAC;AAAA;AACM;AACI;AACgB;AAAA;AAC3B;AAGJ;AAEA;AAIA;AACE;AAKF;AAEA;AACE;AAKF;AAWA;AAAgC;AAC9B;AACA;AACA;AAEF;AACE;AACA;AAEA;AACE;AAA8B;AAGhC;AACE;AAAC;AAAA;AACkD;AAC7C;AACJ;AACe;AACK;AACpB;AACA;AAAA;AAGN;AAEA;AAA6B;AAC3B;AACA;AAEF;AACE;AAEA;AACE;AAAO;AAGT;AAIQ;AACE;AAAC;AAAA;AAEC;AACA;AAAA;AAFgB;AAGlB;AAMZ;AAEA;AAA2D;AAChD;AACW;AAEtB;AAEA;AAAiC;AACb;AACO;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEF;AACE;AACA;AACA;AACA;AACE;AAAO;AACF;AAGC;AACN;AAGF;AAAuE;AAC3B;AAG5C;AACE;AAAqB;AAGvB;AACE;AAAqB;AAGvB;AACE;AAAsB;AAGxB;AAEI;AAAA;AAAmB;AAAlB;AACW;AACD;AACM;AACf;AACA;AACA;AACA;AACO;AAAA;AACT;AAC+D;AAG3D;AACG;AACC;AAAC;AAAA;AACU;AACT;AAAA;AACF;AAEF;AAAC;AAAA;AACU;AACW;AACpB;AAAA;AACF;AAEE;AAAC;AAAA;AACU;AACT;AAAA;AACF;AAEJ;AACiC;AAMvB;AAAC;AAAA;AACgB;AACN;AACC;AACF;AACM;AACE;AAAA;AAM5B;AACF;AAKI;AAAgB;AACb;AAEP;AAIR;AAEa;AAQN;AAAiB;AAEpB;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACW;AACX;AACmB;AACnB;AACA;AACA;AACA;AACA;AACA;AACA;AACe;AACG;AACO;AACzB;AACQ;AACL;AAIL;AAEA;AACA;AACE;AAAU;AACR;AACF;AAGF;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAAyC;AACnB;AACpB;AACA;AAGF;AAEA;AACE;AAAqB;AAGvB;AACE;AAA+B;AAGjC;AAAoB;AAEhB;AAEA;AACE;AAAA;AAGF;AACE;AAAyB;AAC3B;AACF;AACuC;AAGzC;AAAmB;AAEf;AAEA;AACE;AAAA;AAGF;AAAuC;AACL;AAGlC;AACE;AAAwB;AAC1B;AACF;AAC0B;AAG5B;AAA0B;AAEtB;AAEA;AACE;AAAyB;AAC3B;AACF;AAC8B;AAGhC;AAA6B;AAEzB;AAEA;AACE;AAAA;AAGF;AAEA;AACE;AAAY;AACV;AACA;AACc;AACJ;AACW;AACtB;AAED;AAAc;AACZ;AACc;AACJ;AACW;AACtB;AAED;AAAa;AACG;AACd;AACA;AACqB;AACtB;AACH;AACF;AACA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF;AAGF;AAEI;AAAmB;AAAlB;AACmB;AAC6C;AACxD;AACH;AACC;AAC8B;AAC1B;AACD;AACe;AACvB;AACyB;AACF;AACvB;AACA;AAEA;AAAC;AAAA;AACC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe;AACU;AACV;AACf;AACA;AAAA;AACF;AAAA;AAEJ;AAGN;;"}
|
|
1
|
+
{"version":3,"file":"Composer.js","sources":["../../src/components/Composer.tsx"],"sourcesContent":["\"use client\";\n\nimport type {\n BaseMetadata,\n CommentAttachment,\n CommentMixedAttachment,\n DCM,\n DTM,\n GroupMentionData,\n Patchable,\n ThreadVisibility,\n} from \"@liveblocks/core\";\nimport { assertNever, MENTION_CHARACTER } from \"@liveblocks/core\";\nimport { useRoom } from \"@liveblocks/react\";\nimport {\n useCreateRoomComment,\n useCreateRoomThread,\n useEditRoomComment,\n useHasPermissionAccess,\n useLayoutEffect,\n useResolveMentionSuggestions,\n} from \"@liveblocks/react/_private\";\nimport type {\n ComponentPropsWithoutRef,\n ComponentType,\n FocusEvent,\n FormEvent,\n ForwardedRef,\n MouseEvent,\n PropsWithChildren,\n ReactNode,\n RefAttributes,\n SyntheticEvent,\n} from \"react\";\nimport { createContext, forwardRef, useCallback, useMemo, useRef } from \"react\";\n\nimport { useLiveblocksUiConfig } from \"../config\";\nimport { FLOATING_ELEMENT_SIDE_OFFSET } from \"../constants\";\nimport { UsersIcon } from \"../icons\";\nimport { AttachmentIcon } from \"../icons/Attachment\";\nimport { BoldIcon } from \"../icons/Bold\";\nimport { CodeIcon } from \"../icons/Code\";\nimport { EmojiIcon } from \"../icons/Emoji\";\nimport { ItalicIcon } from \"../icons/Italic\";\nimport { MentionIcon } from \"../icons/Mention\";\nimport { SendIcon } from \"../icons/Send\";\nimport { StrikethroughIcon } from \"../icons/Strikethrough\";\nimport type { ComposerOverrides, GlobalOverrides } from \"../overrides\";\nimport { useOverrides } from \"../overrides\";\nimport * as ComposerPrimitive from \"../primitives/Composer\";\nimport {\n useComposer,\n useComposerAttachmentsContext,\n useComposerEditorContext,\n} from \"../primitives/Composer/contexts\";\nimport type {\n ComposerEditorComponents,\n ComposerEditorLinkProps,\n ComposerEditorMentionProps,\n ComposerEditorMentionSuggestionsProps,\n ComposerEditorProps,\n ComposerFormProps,\n ComposerMarkToggleProps,\n ComposerSubmitComment,\n} from \"../primitives/Composer/types\";\nimport { useComposerAttachmentsDropArea } from \"../primitives/Composer/utils\";\nimport { commentsResourceForVisibility } from \"../shared\";\nimport type { ComposerBodyMark } from \"../types\";\nimport { cn } from \"../utils/cn\";\nimport { useControllableState } from \"../utils/use-controllable-state\";\nimport { useIsGroupMentionMember } from \"../utils/use-group-mention\";\nimport { GroupAvatar, UserAvatar } from \"./Avatar\";\nimport { FileAttachment } from \"./internal/Attachment\";\nimport { Attribution } from \"./internal/Attribution\";\nimport { Button } from \"./internal/Button\";\nimport type { EmojiPickerProps } from \"./internal/EmojiPicker\";\nimport { EmojiPicker, EmojiPickerTrigger } from \"./internal/EmojiPicker\";\nimport { Group } from \"./internal/Group\";\nimport { GroupDescription } from \"./internal/GroupDescription\";\nimport { ShortcutTooltip, Tooltip, TooltipProvider } from \"./internal/Tooltip\";\nimport { User } from \"./internal/User\";\n\ninterface EditorActionProps extends ComponentPropsWithoutRef<\"button\"> {\n label: string;\n tooltipLabel?: string;\n}\n\ninterface EmojiEditorActionProps extends EditorActionProps {\n onPickerOpenChange?: EmojiPickerProps[\"onOpenChange\"];\n}\n\ninterface MarkToggleProps extends ComposerMarkToggleProps {\n icon?: ReactNode;\n shortcut?: string;\n}\n\nexport type ComposerCreateThreadProps<\n TM extends BaseMetadata,\n CM extends BaseMetadata,\n> = {\n threadId?: never;\n\n commentId?: never;\n\n /**\n * The metadata of the thread to create.\n */\n metadata?: TM;\n\n /**\n * The visibility of the thread to create.\n */\n visibility?: ThreadVisibility;\n\n /**\n * The metadata of the comment to create.\n */\n commentMetadata?: CM;\n};\n\nexport type ComposerCreateCommentProps<CM extends BaseMetadata> = {\n /**\n * The ID of the thread to reply to.\n */\n threadId: string;\n\n commentId?: never;\n\n metadata?: never;\n\n /**\n * @internal\n * The visibility of the parent thread.\n */\n visibility?: ThreadVisibility;\n\n /**\n * The metadata of the comment to create.\n */\n commentMetadata?: CM;\n};\n\nexport type ComposerEditCommentProps<CM extends BaseMetadata> = {\n /**\n * The ID of the thread to edit a comment in.\n */\n threadId: string;\n\n /**\n * The ID of the comment to edit.\n */\n commentId: string;\n\n metadata?: never;\n\n /**\n * @internal\n * The visibility of the parent thread.\n */\n visibility?: ThreadVisibility;\n\n /**\n * The metadata of the comment to edit.\n */\n commentMetadata?: Patchable<CM>;\n};\n\nexport type ComposerProps<\n TM extends BaseMetadata = DTM,\n CM extends BaseMetadata = DCM,\n> = Omit<ComponentPropsWithoutRef<\"form\">, \"defaultValue\"> &\n (\n | ComposerCreateThreadProps<TM, CM>\n | ComposerCreateCommentProps<CM>\n | ComposerEditCommentProps<CM>\n ) & {\n /**\n * The event handler called when the composer is submitted.\n */\n onComposerSubmit?: (\n comment: ComposerSubmitComment,\n event: FormEvent<HTMLFormElement>\n ) => Promise<void> | void;\n\n /**\n * The composer's initial value.\n */\n defaultValue?: ComposerEditorProps[\"defaultValue\"];\n\n /**\n * The composer's initial attachments.\n */\n defaultAttachments?: CommentAttachment[];\n\n /**\n * Whether the composer is collapsed. Setting a value will make the composer controlled.\n */\n collapsed?: boolean;\n\n /**\n * The event handler called when the collapsed state of the composer changes.\n */\n onCollapsedChange?: (collapsed: boolean) => void;\n\n /**\n * Whether the composer is initially collapsed. Setting a value will make the composer uncontrolled.\n */\n defaultCollapsed?: boolean;\n\n /**\n * Whether to show and allow adding attachments.\n */\n showAttachments?: boolean;\n\n /**\n * Whether to show formatting controls (e.g. a floating toolbar with formatting toggles when selecting text)\n */\n showFormattingControls?: boolean;\n\n /**\n * Whether the composer is disabled.\n */\n disabled?: ComposerFormProps[\"disabled\"];\n\n /**\n * Whether to focus the composer on mount.\n */\n autoFocus?: ComposerEditorProps[\"autoFocus\"];\n\n /**\n * Whether to blur the composer editor when the composer is submitted.\n */\n blurOnSubmit?: boolean;\n\n /**\n * Override the component's strings.\n */\n overrides?: Partial<GlobalOverrides & ComposerOverrides>;\n\n /**\n * @internal\n */\n actions?: ReactNode;\n\n /**\n * @internal\n */\n showAttribution?: boolean;\n\n /**\n * @internal\n */\n roomId?: string;\n };\n\ninterface ComposerEditorContainerProps extends Pick<\n ComposerProps,\n | \"defaultValue\"\n | \"showAttachments\"\n | \"showFormattingControls\"\n | \"showAttribution\"\n | \"overrides\"\n | \"actions\"\n | \"autoFocus\"\n | \"disabled\"\n> {\n isCollapsed: boolean | undefined;\n onEmptyChange: (isEmpty: boolean) => void;\n hasResolveMentionSuggestions: boolean;\n onEmojiPickerOpenChange: (isOpen: boolean) => void;\n onEditorClick: (event: MouseEvent<HTMLDivElement>) => void;\n}\n\ninterface ComposerMentionProps extends ComposerEditorMentionProps {\n overrides?: ComposerProps[\"overrides\"];\n}\n\nfunction ComposerInsertMentionEditorAction({\n label,\n tooltipLabel,\n className,\n onClick,\n ...props\n}: EditorActionProps) {\n const { createMention } = useComposer();\n\n const preventDefault = useCallback((event: SyntheticEvent) => {\n event.preventDefault();\n }, []);\n\n const handleClick = useCallback(\n (event: MouseEvent<HTMLButtonElement>) => {\n onClick?.(event);\n\n if (!event.isDefaultPrevented()) {\n event.stopPropagation();\n createMention();\n }\n },\n [createMention, onClick]\n );\n\n return (\n <Tooltip content={tooltipLabel ?? label}>\n <Button\n className={cn(\"lb-composer-editor-action\", className)}\n onPointerDown={preventDefault}\n onClick={handleClick}\n aria-label={label}\n icon={<MentionIcon />}\n {...props}\n />\n </Tooltip>\n );\n}\n\nfunction ComposerInsertEmojiEditorAction({\n label,\n tooltipLabel,\n onPickerOpenChange,\n className,\n ...props\n}: EmojiEditorActionProps) {\n const { insertText } = useComposer();\n\n const preventDefault = useCallback((event: SyntheticEvent) => {\n event.preventDefault();\n }, []);\n\n const stopPropagation = useCallback((event: SyntheticEvent) => {\n event.stopPropagation();\n }, []);\n\n return (\n <EmojiPicker onEmojiSelect={insertText} onOpenChange={onPickerOpenChange}>\n <Tooltip content={tooltipLabel ?? label}>\n <EmojiPickerTrigger asChild>\n <Button\n className={cn(\"lb-composer-editor-action\", className)}\n onPointerDown={preventDefault}\n onClick={stopPropagation}\n aria-label={label}\n icon={<EmojiIcon />}\n {...props}\n />\n </EmojiPickerTrigger>\n </Tooltip>\n </EmojiPicker>\n );\n}\n\nfunction ComposerAttachFilesEditorAction({\n label,\n tooltipLabel,\n className,\n ...props\n}: EditorActionProps) {\n const preventDefault = useCallback((event: SyntheticEvent) => {\n event.preventDefault();\n }, []);\n\n const stopPropagation = useCallback((event: SyntheticEvent) => {\n event.stopPropagation();\n }, []);\n\n return (\n <Tooltip content={tooltipLabel ?? label}>\n <ComposerPrimitive.AttachFiles asChild>\n <Button\n className={cn(\"lb-composer-editor-action\", className)}\n onPointerDown={preventDefault}\n onClick={stopPropagation}\n aria-label={label}\n icon={<AttachmentIcon />}\n {...props}\n />\n </ComposerPrimitive.AttachFiles>\n </Tooltip>\n );\n}\n\nfunction ComposerUserMention({ mention }: ComposerMentionProps) {\n return (\n <ComposerPrimitive.Mention className=\"lb-mention lb-composer-mention\">\n <span className=\"lb-mention-symbol\">{MENTION_CHARACTER}</span>\n <User userId={mention.id} />\n </ComposerPrimitive.Mention>\n );\n}\n\nfunction ComposerGroupMention({ mention }: ComposerMentionProps) {\n const isMember = useIsGroupMentionMember(mention as GroupMentionData);\n\n return (\n <ComposerPrimitive.Mention\n className=\"lb-mention lb-composer-mention\"\n data-self={isMember ? \"\" : undefined}\n >\n <span className=\"lb-mention-symbol\">{MENTION_CHARACTER}</span>\n <Group groupId={mention.id} />\n </ComposerPrimitive.Mention>\n );\n}\n\nexport function ComposerMention({ mention, ...props }: ComposerMentionProps) {\n switch (mention.kind) {\n case \"user\":\n return <ComposerUserMention mention={mention} {...props} />;\n\n case \"group\":\n return <ComposerGroupMention mention={mention} {...props} />;\n\n default:\n return assertNever(mention, \"Unhandled mention kind\");\n }\n}\n\nfunction ComposerMentionSuggestions({\n mentions,\n}: ComposerEditorMentionSuggestionsProps) {\n return mentions.length > 0 ? (\n <ComposerPrimitive.Suggestions className=\"lb-root lb-portal lb-elevation lb-composer-suggestions lb-composer-mention-suggestions\">\n <ComposerPrimitive.SuggestionsList className=\"lb-composer-suggestions-list lb-composer-mention-suggestions-list\">\n {mentions.map((mention) => {\n return (\n <ComposerPrimitive.SuggestionsListItem\n key={mention.id}\n className=\"lb-composer-suggestions-list-item lb-composer-mention-suggestion\"\n value={mention.id}\n >\n {mention.kind === \"user\" ? (\n <>\n <UserAvatar\n userId={mention.id}\n className=\"lb-composer-mention-suggestion-avatar\"\n />\n <User\n userId={mention.id}\n className=\"lb-composer-mention-suggestion-user\"\n />\n </>\n ) : mention.kind === \"group\" ? (\n <>\n <GroupAvatar\n groupId={mention.id}\n className=\"lb-composer-mention-suggestion-avatar\"\n icon={<UsersIcon />}\n />\n <Group\n groupId={mention.id}\n className=\"lb-composer-mention-suggestion-group\"\n >\n <GroupDescription\n groupId={mention.id}\n className=\"lb-composer-mention-suggestion-group-description\"\n />\n </Group>\n </>\n ) : (\n assertNever(mention, \"Unhandled mention kind\")\n )}\n </ComposerPrimitive.SuggestionsListItem>\n );\n })}\n </ComposerPrimitive.SuggestionsList>\n </ComposerPrimitive.Suggestions>\n ) : null;\n}\n\nfunction MarkToggle({\n mark,\n icon,\n shortcut,\n children,\n ...props\n}: MarkToggleProps) {\n const $ = useOverrides();\n const label = useMemo(() => {\n return $.COMPOSER_TOGGLE_MARK(mark);\n }, [$, mark]);\n\n return (\n <ShortcutTooltip\n content={label}\n shortcut={shortcut}\n sideOffset={FLOATING_ELEMENT_SIDE_OFFSET + 2}\n >\n <ComposerPrimitive.MarkToggle mark={mark} asChild {...props}>\n <Button aria-label={label} variant=\"toolbar\" icon={icon}>\n {children}\n </Button>\n </ComposerPrimitive.MarkToggle>\n </ShortcutTooltip>\n );\n}\n\ntype MarkToggles = {\n [K in ComposerBodyMark]: ComponentType<PropsWithChildren>;\n};\n\nconst markToggles: MarkToggles = {\n bold: () => <MarkToggle mark=\"bold\" shortcut=\"Mod-B\" icon={<BoldIcon />} />,\n italic: () => (\n <MarkToggle mark=\"italic\" shortcut=\"Mod-I\" icon={<ItalicIcon />} />\n ),\n strikethrough: () => (\n <MarkToggle\n mark=\"strikethrough\"\n shortcut=\"Mod-Shift-S\"\n icon={<StrikethroughIcon />}\n />\n ),\n code: () => <MarkToggle mark=\"code\" shortcut=\"Mod-E\" icon={<CodeIcon />} />,\n};\n\nconst markTogglesList = Object.entries(markToggles).map(([mark, Toggle]) => (\n <Toggle key={mark} />\n));\n\nfunction ComposerFloatingToolbar() {\n return (\n <ComposerPrimitive.FloatingToolbar className=\"lb-root lb-portal lb-elevation lb-composer-floating-toolbar\">\n {markTogglesList}\n </ComposerPrimitive.FloatingToolbar>\n );\n}\n\nfunction ComposerLink({ href, children }: ComposerEditorLinkProps) {\n return (\n <ComposerPrimitive.Link href={href} className=\"lb-composer-link\">\n {children}\n </ComposerPrimitive.Link>\n );\n}\n\ninterface ComposerAttachmentsProps extends ComponentPropsWithoutRef<\"div\"> {\n overrides?: Partial<GlobalOverrides & ComposerOverrides>;\n}\n\ninterface ComposerFileAttachmentProps extends ComponentPropsWithoutRef<\"div\"> {\n attachment: CommentMixedAttachment;\n overrides?: Partial<GlobalOverrides & ComposerOverrides>;\n}\n\nfunction ComposerFileAttachment({\n attachment,\n className,\n overrides,\n ...props\n}: ComposerFileAttachmentProps) {\n const { removeAttachment } = useComposer();\n const { roomId } = useComposerEditorContext();\n\n const handleDeleteClick = useCallback(() => {\n removeAttachment(attachment.id);\n }, [attachment.id, removeAttachment]);\n\n return (\n <FileAttachment\n className={cn(\"lb-composer-attachment\", className)}\n {...props}\n attachment={attachment}\n onDeleteClick={handleDeleteClick}\n preventFocusOnDelete\n overrides={overrides}\n roomId={roomId}\n />\n );\n}\n\nfunction ComposerAttachments({\n overrides,\n className,\n ...props\n}: ComposerAttachmentsProps) {\n const { attachments } = useComposer();\n\n if (attachments.length === 0) {\n return null;\n }\n\n return (\n <div className={cn(\"lb-composer-attachments\", className)} {...props}>\n <div className=\"lb-attachments\">\n {attachments.map((attachment) => {\n return (\n <ComposerFileAttachment\n key={attachment.id}\n attachment={attachment}\n overrides={overrides}\n />\n );\n })}\n </div>\n </div>\n );\n}\n\nconst editorRequiredComponents: ComposerEditorComponents = {\n Mention: ComposerMention,\n MentionSuggestions: ComposerMentionSuggestions,\n Link: ComposerLink,\n};\n\nfunction ComposerEditorContainer({\n showAttachments = true,\n showFormattingControls = true,\n showAttribution,\n defaultValue,\n isCollapsed,\n overrides,\n actions,\n autoFocus,\n disabled,\n hasResolveMentionSuggestions,\n onEmojiPickerOpenChange,\n onEmptyChange,\n onEditorClick,\n}: ComposerEditorContainerProps) {\n const { isEmpty } = useComposer();\n const { hasMaxAttachments } = useComposerAttachmentsContext();\n const $ = useOverrides(overrides);\n const components = useMemo(() => {\n return {\n ...editorRequiredComponents,\n FloatingToolbar: showFormattingControls\n ? ComposerFloatingToolbar\n : undefined,\n };\n }, [showFormattingControls]);\n\n const [isDraggingOver, dropAreaProps] = useComposerAttachmentsDropArea({\n disabled: disabled || !showAttachments || hasMaxAttachments,\n });\n\n useLayoutEffect(() => {\n onEmptyChange(isEmpty);\n }, [isEmpty, onEmptyChange]);\n\n const preventDefault = useCallback((event: SyntheticEvent) => {\n event.preventDefault();\n }, []);\n\n const stopPropagation = useCallback((event: SyntheticEvent) => {\n event.stopPropagation();\n }, []);\n\n return (\n <div className=\"lb-composer-editor-container\" {...dropAreaProps}>\n <ComposerPrimitive.Editor\n className=\"lb-composer-editor\"\n onClick={onEditorClick}\n placeholder={$.COMPOSER_PLACEHOLDER}\n defaultValue={defaultValue}\n autoFocus={autoFocus}\n components={components}\n disabled={disabled}\n dir={$.dir}\n />\n {showAttachments && <ComposerAttachments overrides={overrides} />}\n {(!isCollapsed || isDraggingOver) && (\n <div className=\"lb-composer-footer\">\n <div className=\"lb-composer-editor-actions\">\n {hasResolveMentionSuggestions && (\n <ComposerInsertMentionEditorAction\n label={$.COMPOSER_INSERT_MENTION}\n disabled={disabled}\n />\n )}\n <ComposerInsertEmojiEditorAction\n label={$.COMPOSER_INSERT_EMOJI}\n onPickerOpenChange={onEmojiPickerOpenChange}\n disabled={disabled}\n />\n {showAttachments && (\n <ComposerAttachFilesEditorAction\n label={$.COMPOSER_ATTACH_FILES}\n disabled={disabled}\n />\n )}\n </div>\n {showAttribution && <Attribution />}\n <div className=\"lb-composer-actions\">\n {actions ?? (\n <>\n <ShortcutTooltip content={$.COMPOSER_SEND} shortcut=\"Enter\">\n <ComposerPrimitive.Submit asChild>\n <Button\n onPointerDown={preventDefault}\n onClick={stopPropagation}\n className=\"lb-composer-action\"\n variant=\"primary\"\n aria-label={$.COMPOSER_SEND}\n icon={<SendIcon />}\n />\n </ComposerPrimitive.Submit>\n </ShortcutTooltip>\n </>\n )}\n </div>\n </div>\n )}\n {showAttachments && isDraggingOver && (\n <div className=\"lb-composer-attachments-drop-area\">\n <div className=\"lb-composer-attachments-drop-area-label\">\n <AttachmentIcon />\n {$.COMPOSER_ATTACH_FILES}\n </div>\n </div>\n )}\n </div>\n );\n}\n\nexport const ComposerRoomIdContext = createContext<string | null>(null);\n\n/**\n * Displays a composer to create comments.\n *\n * @example\n * <Composer />\n */\nexport const Composer = forwardRef(\n <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n {\n threadId,\n commentId,\n metadata,\n visibility,\n commentMetadata,\n defaultValue,\n defaultAttachments,\n onComposerSubmit,\n collapsed: controlledCollapsed,\n defaultCollapsed,\n onCollapsedChange: controlledOnCollapsedChange,\n overrides,\n actions,\n onBlur,\n className,\n onFocus,\n autoFocus,\n disabled,\n blurOnSubmit = true,\n showAttachments = true,\n showFormattingControls = true,\n showAttribution,\n roomId: _roomId,\n ...props\n }: ComposerProps<TM, CM>,\n forwardedRef: ForwardedRef<HTMLFormElement>\n ) => {\n const room = useRoom({ allowOutsideRoom: true });\n\n const roomId = _roomId !== undefined ? _roomId : room?.id;\n if (roomId === undefined) {\n throw new Error(\n \"Composer must be a descendant of RoomProvider component\"\n );\n }\n\n const createThread = useCreateRoomThread(roomId);\n const createComment = useCreateRoomComment(roomId);\n const editComment = useEditRoomComment(roomId);\n const { preventUnsavedComposerChanges } = useLiveblocksUiConfig();\n const hasResolveMentionSuggestions =\n useResolveMentionSuggestions() !== undefined;\n const isEmptyRef = useRef(true);\n const isEmojiPickerOpenRef = useRef(false);\n const $ = useOverrides(overrides);\n const [isCollapsed, onCollapsedChange] = useControllableState(\n defaultCollapsed ?? false,\n controlledCollapsed,\n controlledOnCollapsedChange\n );\n const commentsResource =\n threadId === undefined\n ? commentsResourceForVisibility(visibility ?? \"public\")\n : commentsResourceForVisibility(visibility);\n const canComment = useHasPermissionAccess(\n roomId,\n commentsResource,\n \"write\"\n );\n\n const setEmptyRef = useCallback((isEmpty: boolean) => {\n isEmptyRef.current = isEmpty;\n }, []);\n\n const setEmojiPickerOpenRef = useCallback((isEmojiPickerOpen: boolean) => {\n isEmojiPickerOpenRef.current = isEmojiPickerOpen;\n }, []);\n\n const handleFocus = useCallback(\n (event: FocusEvent<HTMLFormElement>) => {\n onFocus?.(event);\n\n if (event.isDefaultPrevented()) {\n return;\n }\n\n if (isEmptyRef.current && canComment) {\n onCollapsedChange?.(false);\n }\n },\n [onCollapsedChange, onFocus, canComment]\n );\n\n const handleBlur = useCallback(\n (event: FocusEvent<HTMLFormElement>) => {\n onBlur?.(event);\n\n if (event.isDefaultPrevented()) {\n return;\n }\n\n const isOutside = !event.currentTarget.contains(\n event.relatedTarget ?? document.activeElement\n );\n\n if (isOutside && isEmptyRef.current && !isEmojiPickerOpenRef.current) {\n onCollapsedChange?.(true);\n }\n },\n [onBlur, onCollapsedChange]\n );\n\n const handleEditorClick = useCallback(\n (event: MouseEvent<HTMLDivElement>) => {\n event.stopPropagation();\n\n if (isEmptyRef.current && canComment) {\n onCollapsedChange?.(false);\n }\n },\n [onCollapsedChange, canComment]\n );\n\n const handleComposerSubmit = useCallback(\n (comment: ComposerSubmitComment, event: FormEvent<HTMLFormElement>) => {\n onComposerSubmit?.(comment, event);\n\n if (event.isDefaultPrevented()) {\n return;\n }\n\n event.stopPropagation();\n\n if (commentId && threadId) {\n editComment({\n commentId,\n threadId,\n body: comment.body,\n metadata: commentMetadata,\n attachments: comment.attachments,\n });\n } else if (threadId) {\n createComment({\n threadId,\n body: comment.body,\n metadata: commentMetadata as CM | undefined,\n attachments: comment.attachments,\n });\n } else {\n createThread({\n body: comment.body,\n metadata,\n visibility,\n commentMetadata: commentMetadata as CM | undefined,\n attachments: comment.attachments,\n });\n }\n },\n [\n commentId,\n createComment,\n createThread,\n editComment,\n commentMetadata,\n metadata,\n visibility,\n onComposerSubmit,\n threadId,\n ]\n );\n\n return (\n <TooltipProvider>\n <ComposerPrimitive.Form\n onComposerSubmit={handleComposerSubmit}\n className={cn(\"lb-root lb-composer lb-composer-form\", className)}\n dir={$.dir}\n {...props}\n ref={forwardedRef}\n data-collapsed={isCollapsed ? \"\" : undefined}\n onFocus={handleFocus}\n onBlur={handleBlur}\n disabled={disabled || !canComment}\n defaultAttachments={defaultAttachments}\n pasteFilesAsAttachments={showAttachments}\n preventUnsavedChanges={preventUnsavedComposerChanges}\n blurOnSubmit={blurOnSubmit}\n roomId={roomId}\n >\n <ComposerEditorContainer\n defaultValue={defaultValue}\n actions={actions}\n overrides={overrides}\n isCollapsed={isCollapsed}\n showAttachments={showAttachments}\n showAttribution={showAttribution}\n showFormattingControls={showFormattingControls}\n hasResolveMentionSuggestions={hasResolveMentionSuggestions}\n onEmptyChange={setEmptyRef}\n onEmojiPickerOpenChange={setEmojiPickerOpenRef}\n onEditorClick={handleEditorClick}\n autoFocus={autoFocus}\n disabled={disabled}\n />\n </ComposerPrimitive.Form>\n </TooltipProvider>\n );\n }\n) as <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n props: ComposerProps<TM, CM> & RefAttributes<HTMLFormElement>\n) => JSX.Element;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqRA;AAA2C;AACzC;AACA;AACA;AACA;AAEF;AACE;AAEA;AACE;AAAqB;AAGvB;AAAoB;AAEhB;AAEA;AACE;AACA;AAAc;AAChB;AACF;AACuB;AAGzB;AAEI;AAAC;AAAA;AACqD;AACrC;AACN;AACG;AACO;AACf;AAAA;AAIZ;AAEA;AAAyC;AACvC;AACA;AACA;AACA;AAEF;AACE;AAEA;AACE;AAAqB;AAGvB;AACE;AAAsB;AAGxB;AAIQ;AAAC;AAAA;AACqD;AACrC;AACN;AACG;AACK;AACb;AAAA;AAMhB;AAEA;AAAyC;AACvC;AACA;AACA;AAEF;AACE;AACE;AAAqB;AAGvB;AACE;AAAsB;AAGxB;AAGM;AAAC;AAAA;AACqD;AACrC;AACN;AACG;AACU;AAClB;AAAA;AAKd;AAEA;AACE;AAEI;AAAuD;AAC7B;AAGhC;AAEA;AACE;AAEA;AACE;AAAmB;AAAlB;AACW;AACiB;AAE3B;AAAuD;AAC3B;AAAA;AAAA;AAGlC;AAEO;AACL;AAAsB;AAElB;AAAyD;AAGzD;AAA0D;AAG1D;AAAoD;AAE1D;AAEA;AAAoC;AAEpC;AACE;AAIQ;AACE;AAAmB;AAAlB;AAEW;AACK;AAIX;AAAA;AAAC;AAAA;AACiB;AACN;AAAA;AACZ;AACA;AAAC;AAAA;AACiB;AACN;AAAA;AACZ;AAIA;AAAA;AAAC;AAAA;AACkB;AACP;AACO;AAAA;AACnB;AACA;AAAC;AAAA;AACkB;AACP;AAEV;AAAC;AAAA;AACkB;AACP;AAAA;AACZ;AAAA;AACF;AAG2C;AAAA;AAjClC;AAmCf;AAMZ;AAEA;AAAoB;AAClB;AACA;AACA;AACA;AAEF;AACE;AACA;AACE;AAAkC;AAGpC;AACE;AAAC;AAAA;AACU;AACT;AAC2C;AAM3C;AAAA;AAGN;AAMA;AAAiC;AAC0C;AAEN;AAGjE;AAAC;AAAA;AACM;AACI;AACgB;AAAA;AAC3B;AAGJ;AAEA;AAIA;AACE;AAKF;AAEA;AACE;AAKF;AAWA;AAAgC;AAC9B;AACA;AACA;AAEF;AACE;AACA;AAEA;AACE;AAA8B;AAGhC;AACE;AAAC;AAAA;AACkD;AAC7C;AACJ;AACe;AACK;AACpB;AACA;AAAA;AAGN;AAEA;AAA6B;AAC3B;AACA;AAEF;AACE;AAEA;AACE;AAAO;AAGT;AAIQ;AACE;AAAC;AAAA;AAEC;AACA;AAAA;AAFgB;AAGlB;AAMZ;AAEA;AAA2D;AAChD;AACW;AAEtB;AAEA;AAAiC;AACb;AACO;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEF;AACE;AACA;AACA;AACA;AACE;AAAO;AACF;AAGC;AACN;AAGF;AAAuE;AAC3B;AAG5C;AACE;AAAqB;AAGvB;AACE;AAAqB;AAGvB;AACE;AAAsB;AAGxB;AAEI;AAAA;AAAmB;AAAlB;AACW;AACD;AACM;AACf;AACA;AACA;AACA;AACO;AAAA;AACT;AAC+D;AAG3D;AACG;AACC;AAAC;AAAA;AACU;AACT;AAAA;AACF;AAEF;AAAC;AAAA;AACU;AACW;AACpB;AAAA;AACF;AAEE;AAAC;AAAA;AACU;AACT;AAAA;AACF;AAEJ;AACiC;AAMvB;AAAC;AAAA;AACgB;AACN;AACC;AACF;AACM;AACE;AAAA;AAM5B;AACF;AAKI;AAAgB;AACb;AAEP;AAIR;AAEa;AAQN;AAAiB;AAEpB;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACW;AACX;AACmB;AACnB;AACA;AACA;AACA;AACA;AACA;AACA;AACe;AACG;AACO;AACzB;AACQ;AACL;AAIL;AAEA;AACA;AACE;AAAU;AACR;AACF;AAGF;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAAyC;AACnB;AACpB;AACA;AAEF;AAIA;AAAmB;AACjB;AACA;AACA;AAGF;AACE;AAAqB;AAGvB;AACE;AAA+B;AAGjC;AAAoB;AAEhB;AAEA;AACE;AAAA;AAGF;AACE;AAAyB;AAC3B;AACF;AACuC;AAGzC;AAAmB;AAEf;AAEA;AACE;AAAA;AAGF;AAAuC;AACL;AAGlC;AACE;AAAwB;AAC1B;AACF;AAC0B;AAG5B;AAA0B;AAEtB;AAEA;AACE;AAAyB;AAC3B;AACF;AAC8B;AAGhC;AAA6B;AAEzB;AAEA;AACE;AAAA;AAGF;AAEA;AACE;AAAY;AACV;AACA;AACc;AACJ;AACW;AACtB;AAED;AAAc;AACZ;AACc;AACJ;AACW;AACtB;AAED;AAAa;AACG;AACd;AACA;AACA;AACqB;AACtB;AACH;AACF;AACA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF;AAGF;AAEI;AAAmB;AAAlB;AACmB;AAC6C;AACxD;AACH;AACC;AAC8B;AAC1B;AACD;AACe;AACvB;AACyB;AACF;AACvB;AACA;AAEA;AAAC;AAAA;AACC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe;AACU;AACV;AACf;AACA;AAAA;AACF;AAAA;AAEJ;AAGN;;"}
|
|
@@ -12,6 +12,7 @@ var BellCrossed = require('../icons/BellCrossed.cjs');
|
|
|
12
12
|
var CheckCircle = require('../icons/CheckCircle.cjs');
|
|
13
13
|
var CheckCircleFill = require('../icons/CheckCircleFill.cjs');
|
|
14
14
|
var overrides = require('../overrides.cjs');
|
|
15
|
+
var shared = require('../shared.cjs');
|
|
15
16
|
var cn = require('../utils/cn.cjs');
|
|
16
17
|
var useStableComponent = require('../utils/use-stable-component.cjs');
|
|
17
18
|
var useVisible = require('../utils/use-visible.cjs');
|
|
@@ -167,7 +168,7 @@ const Thread = react.forwardRef(
|
|
|
167
168
|
}, [unreadIndex]);
|
|
168
169
|
const canComment = _private.useHasPermissionAccess(
|
|
169
170
|
thread.roomId,
|
|
170
|
-
|
|
171
|
+
shared.commentsResourceForVisibility(thread.visibility),
|
|
171
172
|
"write"
|
|
172
173
|
);
|
|
173
174
|
const stopPropagation = react.useCallback((event) => {
|
|
@@ -264,6 +265,7 @@ const Thread = react.forwardRef(
|
|
|
264
265
|
className: "lb-thread-comment",
|
|
265
266
|
"data-unread": isUnread ? "" : void 0,
|
|
266
267
|
comment,
|
|
268
|
+
visibility: thread.visibility,
|
|
267
269
|
indentContent: indentCommentContent,
|
|
268
270
|
showDeleted: showDeletedComments,
|
|
269
271
|
showActions,
|
|
@@ -334,6 +336,7 @@ const Thread = react.forwardRef(
|
|
|
334
336
|
{
|
|
335
337
|
className: "lb-thread-composer",
|
|
336
338
|
threadId: thread.id,
|
|
339
|
+
visibility: thread.visibility,
|
|
337
340
|
defaultCollapsed: showComposer === "collapsed" ? true : void 0,
|
|
338
341
|
showAttachments,
|
|
339
342
|
showFormattingControls: showComposerFormattingControls,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Thread.cjs","sources":["../../src/components/Thread.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type BaseMetadata,\n type CommentData,\n type DCM,\n type DTM,\n findLastIndex,\n type ThreadData,\n} from \"@liveblocks/core\";\nimport {\n useHasPermissionAccess,\n useMarkRoomThreadAsRead,\n useMarkRoomThreadAsResolved,\n useMarkRoomThreadAsUnresolved,\n useRoomThreadSubscription,\n} from \"@liveblocks/react/_private\";\nimport { Toggle as TogglePrimitive } from \"radix-ui\";\nimport {\n type ComponentPropsWithoutRef,\n type ComponentType,\n type ForwardedRef,\n forwardRef,\n Fragment,\n type PropsWithChildren,\n type ReactNode,\n type RefAttributes,\n type SyntheticEvent,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport type { GlobalComponents } from \"../components\";\nimport { ArrowDownIcon } from \"../icons/ArrowDown\";\nimport { BellIcon } from \"../icons/Bell\";\nimport { BellCrossedIcon } from \"../icons/BellCrossed\";\nimport { CheckCircleIcon } from \"../icons/CheckCircle\";\nimport { CheckCircleFillIcon } from \"../icons/CheckCircleFill\";\nimport type {\n CommentOverrides,\n ComposerOverrides,\n GlobalOverrides,\n ThreadOverrides,\n} from \"../overrides\";\nimport { useOverrides } from \"../overrides\";\nimport { cn } from \"../utils/cn\";\nimport { useStableComponent } from \"../utils/use-stable-component\";\nimport { useIntersectionCallback } from \"../utils/use-visible\";\nimport { useWindowFocus } from \"../utils/use-window-focus\";\nimport { Comment as DefaultComment, type CommentProps } from \"./Comment\";\nimport { Composer, type ComposerProps } from \"./Composer\";\nimport { Button } from \"./internal/Button\";\nimport { Tooltip, TooltipProvider } from \"./internal/Tooltip\";\n\n// An invisible marker used to mark a thread as read only when it becomes visible.\nfunction MarkThreadAsReadMarker({ thread }: { thread: ThreadData }) {\n const { id: threadId, roomId } = thread;\n const ref = useRef<HTMLDivElement>(null);\n const markThreadAsRead = useMarkRoomThreadAsRead(roomId);\n const isWindowFocused = useWindowFocus();\n\n useIntersectionCallback(\n ref,\n (isIntersecting) => {\n if (isIntersecting) {\n markThreadAsRead(threadId);\n }\n },\n {\n // The underlying IntersectionObserver is only enabled when the window is focused\n enabled: isWindowFocused,\n }\n );\n\n return (\n <div\n ref={ref}\n style={{ height: 0 }}\n aria-hidden\n data-mark-as-read-marker=\"\"\n />\n );\n}\n\nexport interface ThreadComponents<\n // Future components might reference thread metadata so we declare it\n // already to respect the `TM` → `CM` order used everywhere else.\n _TM extends BaseMetadata = DTM,\n CM extends BaseMetadata = DCM,\n> {\n /**\n * The component used to display comments.\n */\n Comment: ComponentType<CommentProps<CM>>;\n}\n\nexport interface ThreadProps<\n TM extends BaseMetadata = DTM,\n CM extends BaseMetadata = DCM,\n> extends ComponentPropsWithoutRef<\"div\"> {\n /**\n * The thread to display.\n */\n thread: ThreadData<TM, CM>;\n\n /**\n * How to show or hide the composer to reply to the thread.\n */\n showComposer?: boolean | \"collapsed\";\n\n /**\n * Whether to show the action to resolve the thread.\n */\n showResolveAction?: boolean;\n\n /**\n * How to show or hide the actions.\n */\n showActions?: CommentProps[\"showActions\"];\n\n /**\n * Whether to show reactions.\n */\n showReactions?: CommentProps[\"showReactions\"];\n\n /**\n * Whether to show the composer's formatting controls.\n */\n showComposerFormattingControls?: ComposerProps[\"showFormattingControls\"];\n\n /**\n * Add (or change) items to display in a comment's dropdown.\n */\n commentDropdownItems?:\n | ReactNode\n | ((props: PropsWithChildren<{ comment: CommentData<CM> }>) => ReactNode);\n\n /**\n * The maximum number of comments to show.\n *\n * The first and last comments are always shown and by default if some comments\n * are hidden, only the first comment will be shown before the \"show more\" button\n * and after it will be shown all the newest comments to fit the limit set.\n *\n * It's possible to customize this by setting `maxVisibleComments` to an object:\n *\n * @example\n * // Only show the last comment, and all the older ones to fit the limit.\n * <Thread maxVisibleComments={{ max: 5, show: \"oldest\" }} />\n *\n * @example\n * // Show as many old comments as new ones to fit the limit.\n * <Thread maxVisibleComments={{ max: 5, show: \"both\" }} />\n */\n maxVisibleComments?:\n | number\n | { max: number; show: \"oldest\" | \"both\" | \"newest\" };\n\n /**\n * Whether to blur the composer editor when the composer is submitted.\n */\n blurComposerOnSubmit?: ComposerProps[\"blurOnSubmit\"];\n\n /**\n * Whether to indent the comments' content.\n */\n indentCommentContent?: CommentProps[\"indentContent\"];\n\n /**\n * Whether to show deleted comments.\n */\n showDeletedComments?: CommentProps[\"showDeleted\"];\n\n /**\n * Whether to show the thread's subscription status.\n */\n showSubscription?: boolean;\n\n /**\n * Whether to show attachments.\n */\n showAttachments?: boolean;\n\n /**\n * The event handler called when changing the resolved status.\n */\n onResolvedChange?: (resolved: boolean) => void;\n\n /**\n * The event handler called when a comment is edited.\n */\n onCommentEdit?: CommentProps[\"onCommentEdit\"];\n\n /**\n * The event handler called when a comment is deleted.\n */\n onCommentDelete?: CommentProps[\"onCommentDelete\"];\n\n /**\n * The event handler called when the thread is deleted.\n * A thread is deleted when all its comments are deleted.\n */\n onThreadDelete?: (thread: ThreadData<TM, CM>) => void;\n\n /**\n * The event handler called when clicking on a comment's author.\n */\n onAuthorClick?: CommentProps[\"onAuthorClick\"];\n\n /**\n * The event handler called when clicking on a mention.\n */\n onMentionClick?: CommentProps[\"onMentionClick\"];\n\n /**\n * The event handler called when clicking on a comment's attachment.\n */\n onAttachmentClick?: CommentProps[\"onAttachmentClick\"];\n\n /**\n * The event handler called when the composer is submitted.\n */\n onComposerSubmit?: ComposerProps[\"onComposerSubmit\"];\n\n /**\n * Whether to focus the composer on mount.\n */\n autoFocus?: ComposerProps[\"autoFocus\"];\n\n /**\n * Override the component's strings.\n */\n overrides?: Partial<\n GlobalOverrides & ThreadOverrides & CommentOverrides & ComposerOverrides\n >;\n\n /**\n * Override the component's components.\n */\n components?: Partial<GlobalComponents & ThreadComponents<TM, CM>>;\n}\n\n/**\n * Displays a thread of comments, with a composer to reply\n * to it.\n *\n * @example\n * <>\n * {threads.map((thread) => (\n * <Thread key={thread.id} thread={thread} />\n * ))}\n * </>\n */\nexport const Thread = forwardRef(\n <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n {\n thread,\n indentCommentContent = true,\n showActions = \"hover\",\n showDeletedComments,\n showResolveAction = true,\n showReactions = true,\n showComposer = \"collapsed\",\n showAttachments = true,\n showSubscription = true,\n showComposerFormattingControls = true,\n maxVisibleComments,\n commentDropdownItems,\n onResolvedChange,\n onCommentEdit,\n onCommentDelete,\n onThreadDelete,\n onAuthorClick,\n onMentionClick,\n onAttachmentClick,\n onComposerSubmit,\n blurComposerOnSubmit,\n autoFocus,\n overrides,\n components,\n className,\n ...props\n }: ThreadProps<TM, CM>,\n forwardedRef: ForwardedRef<HTMLDivElement>\n ) => {\n const Comment = useStableComponent(components?.Comment, DefaultComment);\n const markThreadAsResolved = useMarkRoomThreadAsResolved(thread.roomId);\n const markThreadAsUnresolved = useMarkRoomThreadAsUnresolved(thread.roomId);\n const $ = useOverrides(overrides);\n const [showAllComments, setShowAllComments] = useState(false);\n const firstCommentIndex = useMemo(() => {\n return showDeletedComments\n ? 0\n : thread.comments.findIndex((comment) => comment.body);\n }, [showDeletedComments, thread.comments]);\n const lastCommentIndex = useMemo(() => {\n return showDeletedComments\n ? thread.comments.length - 1\n : findLastIndex(thread.comments, (comment) => Boolean(comment.body));\n }, [showDeletedComments, thread.comments]);\n const commentsCount = useMemo(() => {\n return showDeletedComments\n ? thread.comments.length\n : thread.comments.filter((comment) => comment.body).length;\n }, [showDeletedComments, thread.comments]);\n const hiddenComments = useMemo(() => {\n const maxVisibleCommentsCount =\n typeof maxVisibleComments === \"number\"\n ? maxVisibleComments\n : maxVisibleComments?.max;\n const visibleCommentsShow =\n (typeof maxVisibleComments === \"object\"\n ? maxVisibleComments?.show\n : undefined) ?? \"newest\";\n\n // If we explicitly want to show all comments or there's no limit set,\n // no need to hide any comments.\n if (showAllComments || maxVisibleCommentsCount === undefined) {\n return;\n }\n\n const comments = thread.comments\n .map((comment, index) => ({ comment, index }))\n .filter(({ comment }) => showDeletedComments || comment.body);\n\n // There aren't enough comments so no need to hide any.\n if (comments.length <= Math.max(maxVisibleCommentsCount, 2)) {\n return;\n }\n\n const firstVisibleComment = comments[0]!;\n const lastVisibleComment = comments[comments.length - 1]!;\n\n // Always show the first and last comments even if the limit is set to lower than 2.\n if (maxVisibleCommentsCount <= 2) {\n const firstHiddenCommentIndex =\n comments[1]?.index ?? firstVisibleComment.index;\n const lastHiddenCommentIndex =\n comments[comments.length - 2]?.index ?? lastVisibleComment.index;\n\n return {\n firstIndex: firstHiddenCommentIndex,\n lastIndex: lastHiddenCommentIndex,\n count: comments.slice(1, comments.length - 1).length,\n };\n }\n\n const remainingVisibleCommentsCount = maxVisibleCommentsCount - 2;\n\n // Split the remaining visible comments before, after, or equally.\n const beforeVisibleCommentsCount =\n visibleCommentsShow === \"oldest\"\n ? remainingVisibleCommentsCount\n : visibleCommentsShow === \"newest\"\n ? 0\n : Math.floor(remainingVisibleCommentsCount / 2);\n const afterVisibleCommentsCount =\n visibleCommentsShow === \"oldest\"\n ? 0\n : visibleCommentsShow === \"newest\"\n ? remainingVisibleCommentsCount\n : Math.ceil(remainingVisibleCommentsCount / 2);\n\n // The first comment is always visible so `+ 1` to skip it.\n const firstHiddenComment = comments[1 + beforeVisibleCommentsCount];\n // The last comment is always visible so `- 2` to skip it.\n const lastHiddenComment =\n comments[comments.length - 2 - afterVisibleCommentsCount];\n\n // There aren't any comments to hide besides the first and last ones.\n if (\n !firstHiddenComment ||\n !lastHiddenComment ||\n firstHiddenComment.index > lastHiddenComment.index\n ) {\n return;\n }\n\n return {\n firstIndex: firstHiddenComment.index,\n lastIndex: lastHiddenComment.index,\n count: thread.comments\n .slice(firstHiddenComment.index, lastHiddenComment.index + 1)\n .filter((comment) => showDeletedComments || comment.body).length,\n };\n }, [\n maxVisibleComments,\n showAllComments,\n showDeletedComments,\n thread.comments,\n ]);\n const {\n status: subscriptionStatus,\n unreadSince,\n subscribe,\n unsubscribe,\n } = useRoomThreadSubscription(thread.roomId, thread.id);\n const unreadIndex = useMemo(() => {\n // The user is not subscribed to this thread.\n if (subscriptionStatus !== \"subscribed\") {\n return;\n }\n\n // The user hasn't read the thread yet, so all comments are unread.\n if (unreadSince === null) {\n return firstCommentIndex;\n }\n\n // The user has read the thread, so we find the first unread comment.\n const unreadIndex = thread.comments.findIndex(\n (comment) =>\n (showDeletedComments ? true : comment.body) &&\n comment.createdAt > unreadSince\n );\n\n return unreadIndex >= 0 && unreadIndex < thread.comments.length\n ? unreadIndex\n : undefined;\n }, [\n firstCommentIndex,\n showDeletedComments,\n subscriptionStatus,\n thread.comments,\n unreadSince,\n ]);\n const [newIndex, setNewIndex] = useState<number>();\n const newIndicatorIndex = newIndex === undefined ? unreadIndex : newIndex;\n\n useEffect(() => {\n if (unreadIndex) {\n // Keep the \"new\" indicator at the lowest unread index.\n setNewIndex((persistedUnreadIndex) =>\n Math.min(persistedUnreadIndex ?? Infinity, unreadIndex)\n );\n }\n }, [unreadIndex]);\n\n const canComment = useHasPermissionAccess(\n thread.roomId,\n \"comments\",\n \"write\"\n );\n\n const stopPropagation = useCallback((event: SyntheticEvent) => {\n event.stopPropagation();\n }, []);\n\n const handleResolvedChange = useCallback(\n (resolved: boolean) => {\n onResolvedChange?.(resolved);\n\n if (resolved) {\n markThreadAsResolved(thread.id);\n } else {\n markThreadAsUnresolved(thread.id);\n }\n },\n [\n markThreadAsResolved,\n markThreadAsUnresolved,\n onResolvedChange,\n thread.id,\n ]\n );\n\n const handleCommentDelete = useCallback(\n (comment: CommentData) => {\n onCommentDelete?.(comment);\n\n const filteredComments = thread.comments.filter(\n (comment) => comment.body\n );\n\n if (filteredComments.length <= 1) {\n onThreadDelete?.(thread);\n }\n },\n [onCommentDelete, onThreadDelete, thread]\n );\n\n const handleSubscribeChange = useCallback(() => {\n if (subscriptionStatus === \"subscribed\") {\n unsubscribe();\n } else {\n subscribe();\n }\n }, [subscriptionStatus, subscribe, unsubscribe]);\n\n let currentCommentIndex = 0;\n\n return (\n <TooltipProvider>\n <div\n className={cn(\n \"lb-root lb-thread\",\n showActions === \"hover\" && \"lb-thread:show-actions-hover\",\n className\n )}\n data-resolved={thread.resolved ? \"\" : undefined}\n data-unread={unreadIndex !== undefined ? \"\" : undefined}\n dir={$.dir}\n {...props}\n ref={forwardedRef}\n >\n <div className=\"lb-thread-comments\" role=\"feed\">\n {thread.comments.map((comment, index) => {\n const isNonDeletedComment = showDeletedComments || comment.body;\n const isFirstComment = index === firstCommentIndex;\n const isUnread =\n unreadIndex !== undefined && index >= unreadIndex;\n const isHidden =\n hiddenComments &&\n index >= hiddenComments.firstIndex &&\n index <= hiddenComments.lastIndex;\n const isHiddenBecauseDeleted =\n !showDeletedComments && !comment.body;\n const isFirstHiddenComment =\n isHidden && index === hiddenComments.firstIndex;\n\n if (isFirstHiddenComment) {\n return (\n <div\n key={`${comment.id}:show-more`}\n className=\"lb-thread-show-more\"\n >\n <Button\n variant=\"ghost\"\n className=\"lb-thread-show-more-button\"\n onClick={() => setShowAllComments(true)}\n >\n {$.THREAD_SHOW_MORE_COMMENTS(hiddenComments.count)}\n </Button>\n </div>\n );\n }\n\n if (isHidden || isHiddenBecauseDeleted) {\n return null;\n }\n\n if (isNonDeletedComment) {\n currentCommentIndex++;\n }\n\n const children = (\n <Comment\n key={comment.id}\n tabIndex={0}\n aria-posinset={currentCommentIndex}\n aria-setsize={commentsCount}\n overrides={overrides}\n className=\"lb-thread-comment\"\n data-unread={isUnread ? \"\" : undefined}\n comment={comment}\n indentContent={indentCommentContent}\n showDeleted={showDeletedComments}\n showActions={showActions}\n showReactions={showReactions}\n showAttachments={showAttachments}\n showComposerFormattingControls={\n showComposerFormattingControls\n }\n onCommentEdit={onCommentEdit}\n onCommentDelete={handleCommentDelete}\n onAuthorClick={onAuthorClick}\n onMentionClick={onMentionClick}\n onAttachmentClick={onAttachmentClick}\n components={components}\n actionsClassName={\n isFirstComment ? \"lb-thread-actions\" : undefined\n }\n actions={\n isFirstComment && showResolveAction ? (\n <Tooltip\n content={\n thread.resolved\n ? $.THREAD_UNRESOLVE\n : $.THREAD_RESOLVE\n }\n >\n <TogglePrimitive.Root\n pressed={thread.resolved}\n onPressedChange={handleResolvedChange}\n asChild\n >\n <Button\n className=\"lb-comment-action\"\n onClick={stopPropagation}\n aria-label={\n thread.resolved\n ? $.THREAD_UNRESOLVE\n : $.THREAD_RESOLVE\n }\n icon={\n thread.resolved ? (\n <CheckCircleFillIcon />\n ) : (\n <CheckCircleIcon />\n )\n }\n disabled={!canComment}\n />\n </TogglePrimitive.Root>\n </Tooltip>\n ) : null\n }\n internalDropdownItems={\n isFirstComment && showSubscription ? (\n <DefaultComment.DropdownItem\n onSelect={handleSubscribeChange}\n icon={\n subscriptionStatus === \"subscribed\" ? (\n <BellCrossedIcon />\n ) : (\n <BellIcon />\n )\n }\n >\n {subscriptionStatus === \"subscribed\"\n ? $.THREAD_UNSUBSCRIBE\n : $.THREAD_SUBSCRIBE}\n </DefaultComment.DropdownItem>\n ) : undefined\n }\n dropdownItems={\n commentDropdownItems as CommentProps[\"dropdownItems\"]\n }\n />\n );\n\n return index === newIndicatorIndex &&\n newIndicatorIndex !== firstCommentIndex &&\n newIndicatorIndex <= lastCommentIndex ? (\n <Fragment key={comment.id}>\n <div\n className=\"lb-thread-new-indicator\"\n aria-label={$.THREAD_NEW_INDICATOR_DESCRIPTION}\n >\n <span className=\"lb-thread-new-indicator-label\">\n <ArrowDownIcon className=\"lb-thread-new-indicator-label-icon\" />\n {$.THREAD_NEW_INDICATOR}\n </span>\n </div>\n {children}\n </Fragment>\n ) : (\n children\n );\n })}\n </div>\n {unreadIndex !== undefined && (\n <MarkThreadAsReadMarker thread={thread} />\n )}\n {showComposer && (\n <Composer\n className=\"lb-thread-composer\"\n threadId={thread.id}\n defaultCollapsed={showComposer === \"collapsed\" ? true : undefined}\n showAttachments={showAttachments}\n showFormattingControls={showComposerFormattingControls}\n onComposerSubmit={onComposerSubmit}\n blurOnSubmit={blurComposerOnSubmit}\n overrides={{\n COMPOSER_PLACEHOLDER: $.THREAD_COMPOSER_PLACEHOLDER,\n COMPOSER_SEND: $.THREAD_COMPOSER_SEND,\n ...overrides,\n }}\n roomId={thread.roomId}\n autoFocus={autoFocus}\n />\n )}\n </div>\n </TooltipProvider>\n );\n }\n) as <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n props: ThreadProps<TM, CM> & RefAttributes<HTMLDivElement>\n) => JSX.Element;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA0DA;AACE;AACA;AACA;AACA;AAEA;AAAA;AACE;AAEE;AACE;AAAyB;AAC3B;AACF;AACA;AAAA;AAEW;AACX;AAGF;AACE;AAAC;AAAA;AACC;AACmB;AACR;AACc;AAAA;AAG/B;AA2KO;AAAe;AAElB;AACE;AACuB;AACT;AACd;AACoB;AACJ;AACD;AACG;AACC;AACc;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACG;AAIL;AACA;AACA;AACA;AACA;AACA;AACE;AAEuD;AAEzD;AACE;AAEqE;AAEvE;AACE;AAEsD;AAExD;AACE;AAIA;AAOA;AACE;AAAA;AAGF;AAKA;AACE;AAAA;AAGF;AACA;AAGA;AACE;AAEA;AAGA;AAAO;AACO;AACD;AACmC;AAChD;AAGF;AAGA;AAMA;AAQA;AAEA;AAIA;AAKE;AAAA;AAGF;AAAO;AAC0B;AACF;AAG+B;AAC9D;AACC;AACD;AACA;AACA;AACO;AAET;AAAM;AACI;AACR;AACA;AACA;AAEF;AAEE;AACE;AAAA;AAIF;AACE;AAAO;AAIT;AAAoC;AAGZ;AAGxB;AAEI;AACH;AACD;AACA;AACA;AACO;AACP;AAEF;AACA;AAEA;AACE;AAEE;AAAA;AACwD;AACxD;AACF;AAGF;AAAmB;AACV;AACP;AACA;AAGF;AACE;AAAsB;AAGxB;AAA6B;AAEzB;AAEA;AACE;AAA8B;AAE9B;AAAgC;AAClC;AACF;AACA;AACE;AACA;AACA;AACO;AACT;AAGF;AAA4B;AAExB;AAEA;AAAyC;AAClB;AAGvB;AACE;AAAuB;AACzB;AACF;AACwC;AAG1C;AACE;AACE;AAAY;AAEZ;AAAU;AACZ;AAGF;AAEA;AAEI;AAAC;AAAA;AACY;AACT;AAC2B;AAC3B;AACF;AACsC;AACQ;AACvC;AACH;AACC;AAEL;AAEI;AACA;AACA;AAEA;AAIA;AAEA;AAGA;AACE;AACE;AAAC;AAAA;AAEW;AAEV;AAAC;AAAA;AACS;AACE;AAC4B;AAEW;AAAA;AACnD;AAAA;AATkB;AAUpB;AAIJ;AACE;AAAO;AAGT;AACE;AAAA;AAGF;AACE;AAAC;AAAA;AAEW;AACK;AACD;AACd;AACU;AACmB;AAC7B;AACe;AACF;AACb;AACA;AACA;AACA;AAGA;AACiB;AACjB;AACA;AACA;AACA;AAEyC;AAIrC;AAAC;AAAA;AAIS;AAGR;AAAiB;AAAhB;AACiB;AACC;AACV;AAEP;AAAC;AAAA;AACW;AACD;AAID;AAMa;AAGV;AAAA;AACb;AAAA;AACF;AAAA;AAEA;AAIF;AAAgB;AAAf;AACW;AAKI;AAMR;AAAA;AAEN;AAGJ;AAAA;AA/EW;AAoFjB;AAII;AAAA;AAAC;AAAA;AACW;AACI;AAGZ;AAA8D;AAC3D;AACL;AAAA;AACF;AACC;AAGH;AAGN;AAE0C;AAGxC;AAAC;AAAA;AACW;AACO;AACuC;AACxD;AACwB;AACxB;AACc;AACH;AACe;AACP;AACd;AACL;AACe;AACf;AAAA;AACF;AAAA;AAAA;AAGN;AAGN;;"}
|
|
1
|
+
{"version":3,"file":"Thread.cjs","sources":["../../src/components/Thread.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type BaseMetadata,\n type CommentData,\n type DCM,\n type DTM,\n findLastIndex,\n type ThreadData,\n} from \"@liveblocks/core\";\nimport {\n useHasPermissionAccess,\n useMarkRoomThreadAsRead,\n useMarkRoomThreadAsResolved,\n useMarkRoomThreadAsUnresolved,\n useRoomThreadSubscription,\n} from \"@liveblocks/react/_private\";\nimport { Toggle as TogglePrimitive } from \"radix-ui\";\nimport {\n type ComponentPropsWithoutRef,\n type ComponentType,\n type ForwardedRef,\n forwardRef,\n Fragment,\n type PropsWithChildren,\n type ReactNode,\n type RefAttributes,\n type SyntheticEvent,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport type { GlobalComponents } from \"../components\";\nimport { ArrowDownIcon } from \"../icons/ArrowDown\";\nimport { BellIcon } from \"../icons/Bell\";\nimport { BellCrossedIcon } from \"../icons/BellCrossed\";\nimport { CheckCircleIcon } from \"../icons/CheckCircle\";\nimport { CheckCircleFillIcon } from \"../icons/CheckCircleFill\";\nimport type {\n CommentOverrides,\n ComposerOverrides,\n GlobalOverrides,\n ThreadOverrides,\n} from \"../overrides\";\nimport { useOverrides } from \"../overrides\";\nimport { commentsResourceForVisibility } from \"../shared\";\nimport { cn } from \"../utils/cn\";\nimport { useStableComponent } from \"../utils/use-stable-component\";\nimport { useIntersectionCallback } from \"../utils/use-visible\";\nimport { useWindowFocus } from \"../utils/use-window-focus\";\nimport { Comment as DefaultComment, type CommentProps } from \"./Comment\";\nimport { Composer, type ComposerProps } from \"./Composer\";\nimport { Button } from \"./internal/Button\";\nimport { Tooltip, TooltipProvider } from \"./internal/Tooltip\";\n\n// An invisible marker used to mark a thread as read only when it becomes visible.\nfunction MarkThreadAsReadMarker({ thread }: { thread: ThreadData }) {\n const { id: threadId, roomId } = thread;\n const ref = useRef<HTMLDivElement>(null);\n const markThreadAsRead = useMarkRoomThreadAsRead(roomId);\n const isWindowFocused = useWindowFocus();\n\n useIntersectionCallback(\n ref,\n (isIntersecting) => {\n if (isIntersecting) {\n markThreadAsRead(threadId);\n }\n },\n {\n // The underlying IntersectionObserver is only enabled when the window is focused\n enabled: isWindowFocused,\n }\n );\n\n return (\n <div\n ref={ref}\n style={{ height: 0 }}\n aria-hidden\n data-mark-as-read-marker=\"\"\n />\n );\n}\n\nexport interface ThreadComponents<\n // Future components might reference thread metadata so we declare it\n // already to respect the `TM` → `CM` order used everywhere else.\n _TM extends BaseMetadata = DTM,\n CM extends BaseMetadata = DCM,\n> {\n /**\n * The component used to display comments.\n */\n Comment: ComponentType<CommentProps<CM>>;\n}\n\nexport interface ThreadProps<\n TM extends BaseMetadata = DTM,\n CM extends BaseMetadata = DCM,\n> extends ComponentPropsWithoutRef<\"div\"> {\n /**\n * The thread to display.\n */\n thread: ThreadData<TM, CM>;\n\n /**\n * How to show or hide the composer to reply to the thread.\n */\n showComposer?: boolean | \"collapsed\";\n\n /**\n * Whether to show the action to resolve the thread.\n */\n showResolveAction?: boolean;\n\n /**\n * How to show or hide the actions.\n */\n showActions?: CommentProps[\"showActions\"];\n\n /**\n * Whether to show reactions.\n */\n showReactions?: CommentProps[\"showReactions\"];\n\n /**\n * Whether to show the composer's formatting controls.\n */\n showComposerFormattingControls?: ComposerProps[\"showFormattingControls\"];\n\n /**\n * Add (or change) items to display in a comment's dropdown.\n */\n commentDropdownItems?:\n | ReactNode\n | ((props: PropsWithChildren<{ comment: CommentData<CM> }>) => ReactNode);\n\n /**\n * The maximum number of comments to show.\n *\n * The first and last comments are always shown and by default if some comments\n * are hidden, only the first comment will be shown before the \"show more\" button\n * and after it will be shown all the newest comments to fit the limit set.\n *\n * It's possible to customize this by setting `maxVisibleComments` to an object:\n *\n * @example\n * // Only show the last comment, and all the older ones to fit the limit.\n * <Thread maxVisibleComments={{ max: 5, show: \"oldest\" }} />\n *\n * @example\n * // Show as many old comments as new ones to fit the limit.\n * <Thread maxVisibleComments={{ max: 5, show: \"both\" }} />\n */\n maxVisibleComments?:\n | number\n | { max: number; show: \"oldest\" | \"both\" | \"newest\" };\n\n /**\n * Whether to blur the composer editor when the composer is submitted.\n */\n blurComposerOnSubmit?: ComposerProps[\"blurOnSubmit\"];\n\n /**\n * Whether to indent the comments' content.\n */\n indentCommentContent?: CommentProps[\"indentContent\"];\n\n /**\n * Whether to show deleted comments.\n */\n showDeletedComments?: CommentProps[\"showDeleted\"];\n\n /**\n * Whether to show the thread's subscription status.\n */\n showSubscription?: boolean;\n\n /**\n * Whether to show attachments.\n */\n showAttachments?: boolean;\n\n /**\n * The event handler called when changing the resolved status.\n */\n onResolvedChange?: (resolved: boolean) => void;\n\n /**\n * The event handler called when a comment is edited.\n */\n onCommentEdit?: CommentProps[\"onCommentEdit\"];\n\n /**\n * The event handler called when a comment is deleted.\n */\n onCommentDelete?: CommentProps[\"onCommentDelete\"];\n\n /**\n * The event handler called when the thread is deleted.\n * A thread is deleted when all its comments are deleted.\n */\n onThreadDelete?: (thread: ThreadData<TM, CM>) => void;\n\n /**\n * The event handler called when clicking on a comment's author.\n */\n onAuthorClick?: CommentProps[\"onAuthorClick\"];\n\n /**\n * The event handler called when clicking on a mention.\n */\n onMentionClick?: CommentProps[\"onMentionClick\"];\n\n /**\n * The event handler called when clicking on a comment's attachment.\n */\n onAttachmentClick?: CommentProps[\"onAttachmentClick\"];\n\n /**\n * The event handler called when the composer is submitted.\n */\n onComposerSubmit?: ComposerProps[\"onComposerSubmit\"];\n\n /**\n * Whether to focus the composer on mount.\n */\n autoFocus?: ComposerProps[\"autoFocus\"];\n\n /**\n * Override the component's strings.\n */\n overrides?: Partial<\n GlobalOverrides & ThreadOverrides & CommentOverrides & ComposerOverrides\n >;\n\n /**\n * Override the component's components.\n */\n components?: Partial<GlobalComponents & ThreadComponents<TM, CM>>;\n}\n\n/**\n * Displays a thread of comments, with a composer to reply\n * to it.\n *\n * @example\n * <>\n * {threads.map((thread) => (\n * <Thread key={thread.id} thread={thread} />\n * ))}\n * </>\n */\nexport const Thread = forwardRef(\n <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n {\n thread,\n indentCommentContent = true,\n showActions = \"hover\",\n showDeletedComments,\n showResolveAction = true,\n showReactions = true,\n showComposer = \"collapsed\",\n showAttachments = true,\n showSubscription = true,\n showComposerFormattingControls = true,\n maxVisibleComments,\n commentDropdownItems,\n onResolvedChange,\n onCommentEdit,\n onCommentDelete,\n onThreadDelete,\n onAuthorClick,\n onMentionClick,\n onAttachmentClick,\n onComposerSubmit,\n blurComposerOnSubmit,\n autoFocus,\n overrides,\n components,\n className,\n ...props\n }: ThreadProps<TM, CM>,\n forwardedRef: ForwardedRef<HTMLDivElement>\n ) => {\n const Comment = useStableComponent(components?.Comment, DefaultComment);\n const markThreadAsResolved = useMarkRoomThreadAsResolved(thread.roomId);\n const markThreadAsUnresolved = useMarkRoomThreadAsUnresolved(thread.roomId);\n const $ = useOverrides(overrides);\n const [showAllComments, setShowAllComments] = useState(false);\n const firstCommentIndex = useMemo(() => {\n return showDeletedComments\n ? 0\n : thread.comments.findIndex((comment) => comment.body);\n }, [showDeletedComments, thread.comments]);\n const lastCommentIndex = useMemo(() => {\n return showDeletedComments\n ? thread.comments.length - 1\n : findLastIndex(thread.comments, (comment) => Boolean(comment.body));\n }, [showDeletedComments, thread.comments]);\n const commentsCount = useMemo(() => {\n return showDeletedComments\n ? thread.comments.length\n : thread.comments.filter((comment) => comment.body).length;\n }, [showDeletedComments, thread.comments]);\n const hiddenComments = useMemo(() => {\n const maxVisibleCommentsCount =\n typeof maxVisibleComments === \"number\"\n ? maxVisibleComments\n : maxVisibleComments?.max;\n const visibleCommentsShow =\n (typeof maxVisibleComments === \"object\"\n ? maxVisibleComments?.show\n : undefined) ?? \"newest\";\n\n // If we explicitly want to show all comments or there's no limit set,\n // no need to hide any comments.\n if (showAllComments || maxVisibleCommentsCount === undefined) {\n return;\n }\n\n const comments = thread.comments\n .map((comment, index) => ({ comment, index }))\n .filter(({ comment }) => showDeletedComments || comment.body);\n\n // There aren't enough comments so no need to hide any.\n if (comments.length <= Math.max(maxVisibleCommentsCount, 2)) {\n return;\n }\n\n const firstVisibleComment = comments[0]!;\n const lastVisibleComment = comments[comments.length - 1]!;\n\n // Always show the first and last comments even if the limit is set to lower than 2.\n if (maxVisibleCommentsCount <= 2) {\n const firstHiddenCommentIndex =\n comments[1]?.index ?? firstVisibleComment.index;\n const lastHiddenCommentIndex =\n comments[comments.length - 2]?.index ?? lastVisibleComment.index;\n\n return {\n firstIndex: firstHiddenCommentIndex,\n lastIndex: lastHiddenCommentIndex,\n count: comments.slice(1, comments.length - 1).length,\n };\n }\n\n const remainingVisibleCommentsCount = maxVisibleCommentsCount - 2;\n\n // Split the remaining visible comments before, after, or equally.\n const beforeVisibleCommentsCount =\n visibleCommentsShow === \"oldest\"\n ? remainingVisibleCommentsCount\n : visibleCommentsShow === \"newest\"\n ? 0\n : Math.floor(remainingVisibleCommentsCount / 2);\n const afterVisibleCommentsCount =\n visibleCommentsShow === \"oldest\"\n ? 0\n : visibleCommentsShow === \"newest\"\n ? remainingVisibleCommentsCount\n : Math.ceil(remainingVisibleCommentsCount / 2);\n\n // The first comment is always visible so `+ 1` to skip it.\n const firstHiddenComment = comments[1 + beforeVisibleCommentsCount];\n // The last comment is always visible so `- 2` to skip it.\n const lastHiddenComment =\n comments[comments.length - 2 - afterVisibleCommentsCount];\n\n // There aren't any comments to hide besides the first and last ones.\n if (\n !firstHiddenComment ||\n !lastHiddenComment ||\n firstHiddenComment.index > lastHiddenComment.index\n ) {\n return;\n }\n\n return {\n firstIndex: firstHiddenComment.index,\n lastIndex: lastHiddenComment.index,\n count: thread.comments\n .slice(firstHiddenComment.index, lastHiddenComment.index + 1)\n .filter((comment) => showDeletedComments || comment.body).length,\n };\n }, [\n maxVisibleComments,\n showAllComments,\n showDeletedComments,\n thread.comments,\n ]);\n const {\n status: subscriptionStatus,\n unreadSince,\n subscribe,\n unsubscribe,\n } = useRoomThreadSubscription(thread.roomId, thread.id);\n const unreadIndex = useMemo(() => {\n // The user is not subscribed to this thread.\n if (subscriptionStatus !== \"subscribed\") {\n return;\n }\n\n // The user hasn't read the thread yet, so all comments are unread.\n if (unreadSince === null) {\n return firstCommentIndex;\n }\n\n // The user has read the thread, so we find the first unread comment.\n const unreadIndex = thread.comments.findIndex(\n (comment) =>\n (showDeletedComments ? true : comment.body) &&\n comment.createdAt > unreadSince\n );\n\n return unreadIndex >= 0 && unreadIndex < thread.comments.length\n ? unreadIndex\n : undefined;\n }, [\n firstCommentIndex,\n showDeletedComments,\n subscriptionStatus,\n thread.comments,\n unreadSince,\n ]);\n const [newIndex, setNewIndex] = useState<number>();\n const newIndicatorIndex = newIndex === undefined ? unreadIndex : newIndex;\n\n useEffect(() => {\n if (unreadIndex) {\n // Keep the \"new\" indicator at the lowest unread index.\n setNewIndex((persistedUnreadIndex) =>\n Math.min(persistedUnreadIndex ?? Infinity, unreadIndex)\n );\n }\n }, [unreadIndex]);\n\n const canComment = useHasPermissionAccess(\n thread.roomId,\n commentsResourceForVisibility(thread.visibility),\n \"write\"\n );\n\n const stopPropagation = useCallback((event: SyntheticEvent) => {\n event.stopPropagation();\n }, []);\n\n const handleResolvedChange = useCallback(\n (resolved: boolean) => {\n onResolvedChange?.(resolved);\n\n if (resolved) {\n markThreadAsResolved(thread.id);\n } else {\n markThreadAsUnresolved(thread.id);\n }\n },\n [\n markThreadAsResolved,\n markThreadAsUnresolved,\n onResolvedChange,\n thread.id,\n ]\n );\n\n const handleCommentDelete = useCallback(\n (comment: CommentData) => {\n onCommentDelete?.(comment);\n\n const filteredComments = thread.comments.filter(\n (comment) => comment.body\n );\n\n if (filteredComments.length <= 1) {\n onThreadDelete?.(thread);\n }\n },\n [onCommentDelete, onThreadDelete, thread]\n );\n\n const handleSubscribeChange = useCallback(() => {\n if (subscriptionStatus === \"subscribed\") {\n unsubscribe();\n } else {\n subscribe();\n }\n }, [subscriptionStatus, subscribe, unsubscribe]);\n\n let currentCommentIndex = 0;\n\n return (\n <TooltipProvider>\n <div\n className={cn(\n \"lb-root lb-thread\",\n showActions === \"hover\" && \"lb-thread:show-actions-hover\",\n className\n )}\n data-resolved={thread.resolved ? \"\" : undefined}\n data-unread={unreadIndex !== undefined ? \"\" : undefined}\n dir={$.dir}\n {...props}\n ref={forwardedRef}\n >\n <div className=\"lb-thread-comments\" role=\"feed\">\n {thread.comments.map((comment, index) => {\n const isNonDeletedComment = showDeletedComments || comment.body;\n const isFirstComment = index === firstCommentIndex;\n const isUnread =\n unreadIndex !== undefined && index >= unreadIndex;\n const isHidden =\n hiddenComments &&\n index >= hiddenComments.firstIndex &&\n index <= hiddenComments.lastIndex;\n const isHiddenBecauseDeleted =\n !showDeletedComments && !comment.body;\n const isFirstHiddenComment =\n isHidden && index === hiddenComments.firstIndex;\n\n if (isFirstHiddenComment) {\n return (\n <div\n key={`${comment.id}:show-more`}\n className=\"lb-thread-show-more\"\n >\n <Button\n variant=\"ghost\"\n className=\"lb-thread-show-more-button\"\n onClick={() => setShowAllComments(true)}\n >\n {$.THREAD_SHOW_MORE_COMMENTS(hiddenComments.count)}\n </Button>\n </div>\n );\n }\n\n if (isHidden || isHiddenBecauseDeleted) {\n return null;\n }\n\n if (isNonDeletedComment) {\n currentCommentIndex++;\n }\n\n const children = (\n <Comment\n key={comment.id}\n tabIndex={0}\n aria-posinset={currentCommentIndex}\n aria-setsize={commentsCount}\n overrides={overrides}\n className=\"lb-thread-comment\"\n data-unread={isUnread ? \"\" : undefined}\n comment={comment}\n visibility={thread.visibility}\n indentContent={indentCommentContent}\n showDeleted={showDeletedComments}\n showActions={showActions}\n showReactions={showReactions}\n showAttachments={showAttachments}\n showComposerFormattingControls={\n showComposerFormattingControls\n }\n onCommentEdit={onCommentEdit}\n onCommentDelete={handleCommentDelete}\n onAuthorClick={onAuthorClick}\n onMentionClick={onMentionClick}\n onAttachmentClick={onAttachmentClick}\n components={components}\n actionsClassName={\n isFirstComment ? \"lb-thread-actions\" : undefined\n }\n actions={\n isFirstComment && showResolveAction ? (\n <Tooltip\n content={\n thread.resolved\n ? $.THREAD_UNRESOLVE\n : $.THREAD_RESOLVE\n }\n >\n <TogglePrimitive.Root\n pressed={thread.resolved}\n onPressedChange={handleResolvedChange}\n asChild\n >\n <Button\n className=\"lb-comment-action\"\n onClick={stopPropagation}\n aria-label={\n thread.resolved\n ? $.THREAD_UNRESOLVE\n : $.THREAD_RESOLVE\n }\n icon={\n thread.resolved ? (\n <CheckCircleFillIcon />\n ) : (\n <CheckCircleIcon />\n )\n }\n disabled={!canComment}\n />\n </TogglePrimitive.Root>\n </Tooltip>\n ) : null\n }\n internalDropdownItems={\n isFirstComment && showSubscription ? (\n <DefaultComment.DropdownItem\n onSelect={handleSubscribeChange}\n icon={\n subscriptionStatus === \"subscribed\" ? (\n <BellCrossedIcon />\n ) : (\n <BellIcon />\n )\n }\n >\n {subscriptionStatus === \"subscribed\"\n ? $.THREAD_UNSUBSCRIBE\n : $.THREAD_SUBSCRIBE}\n </DefaultComment.DropdownItem>\n ) : undefined\n }\n dropdownItems={\n commentDropdownItems as CommentProps[\"dropdownItems\"]\n }\n />\n );\n\n return index === newIndicatorIndex &&\n newIndicatorIndex !== firstCommentIndex &&\n newIndicatorIndex <= lastCommentIndex ? (\n <Fragment key={comment.id}>\n <div\n className=\"lb-thread-new-indicator\"\n aria-label={$.THREAD_NEW_INDICATOR_DESCRIPTION}\n >\n <span className=\"lb-thread-new-indicator-label\">\n <ArrowDownIcon className=\"lb-thread-new-indicator-label-icon\" />\n {$.THREAD_NEW_INDICATOR}\n </span>\n </div>\n {children}\n </Fragment>\n ) : (\n children\n );\n })}\n </div>\n {unreadIndex !== undefined && (\n <MarkThreadAsReadMarker thread={thread} />\n )}\n {showComposer && (\n <Composer\n className=\"lb-thread-composer\"\n threadId={thread.id}\n visibility={thread.visibility}\n defaultCollapsed={showComposer === \"collapsed\" ? true : undefined}\n showAttachments={showAttachments}\n showFormattingControls={showComposerFormattingControls}\n onComposerSubmit={onComposerSubmit}\n blurOnSubmit={blurComposerOnSubmit}\n overrides={{\n COMPOSER_PLACEHOLDER: $.THREAD_COMPOSER_PLACEHOLDER,\n COMPOSER_SEND: $.THREAD_COMPOSER_SEND,\n ...overrides,\n }}\n roomId={thread.roomId}\n autoFocus={autoFocus}\n />\n )}\n </div>\n </TooltipProvider>\n );\n }\n) as <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n props: ThreadProps<TM, CM> & RefAttributes<HTMLDivElement>\n) => JSX.Element;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA2DA;AACE;AACA;AACA;AACA;AAEA;AAAA;AACE;AAEE;AACE;AAAyB;AAC3B;AACF;AACA;AAAA;AAEW;AACX;AAGF;AACE;AAAC;AAAA;AACC;AACmB;AACR;AACc;AAAA;AAG/B;AA2KO;AAAe;AAElB;AACE;AACuB;AACT;AACd;AACoB;AACJ;AACD;AACG;AACC;AACc;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACG;AAIL;AACA;AACA;AACA;AACA;AACA;AACE;AAEuD;AAEzD;AACE;AAEqE;AAEvE;AACE;AAEsD;AAExD;AACE;AAIA;AAOA;AACE;AAAA;AAGF;AAKA;AACE;AAAA;AAGF;AACA;AAGA;AACE;AAEA;AAGA;AAAO;AACO;AACD;AACmC;AAChD;AAGF;AAGA;AAMA;AAQA;AAEA;AAIA;AAKE;AAAA;AAGF;AAAO;AAC0B;AACF;AAG+B;AAC9D;AACC;AACD;AACA;AACA;AACO;AAET;AAAM;AACI;AACR;AACA;AACA;AAEF;AAEE;AACE;AAAA;AAIF;AACE;AAAO;AAIT;AAAoC;AAGZ;AAGxB;AAEI;AACH;AACD;AACA;AACA;AACO;AACP;AAEF;AACA;AAEA;AACE;AAEE;AAAA;AACwD;AACxD;AACF;AAGF;AAAmB;AACV;AACwC;AAC/C;AAGF;AACE;AAAsB;AAGxB;AAA6B;AAEzB;AAEA;AACE;AAA8B;AAE9B;AAAgC;AAClC;AACF;AACA;AACE;AACA;AACA;AACO;AACT;AAGF;AAA4B;AAExB;AAEA;AAAyC;AAClB;AAGvB;AACE;AAAuB;AACzB;AACF;AACwC;AAG1C;AACE;AACE;AAAY;AAEZ;AAAU;AACZ;AAGF;AAEA;AAEI;AAAC;AAAA;AACY;AACT;AAC2B;AAC3B;AACF;AACsC;AACQ;AACvC;AACH;AACC;AAEL;AAEI;AACA;AACA;AAEA;AAIA;AAEA;AAGA;AACE;AACE;AAAC;AAAA;AAEW;AAEV;AAAC;AAAA;AACS;AACE;AAC4B;AAEW;AAAA;AACnD;AAAA;AATkB;AAUpB;AAIJ;AACE;AAAO;AAGT;AACE;AAAA;AAGF;AACE;AAAC;AAAA;AAEW;AACK;AACD;AACd;AACU;AACmB;AAC7B;AACmB;AACJ;AACF;AACb;AACA;AACA;AACA;AAGA;AACiB;AACjB;AACA;AACA;AACA;AAEyC;AAIrC;AAAC;AAAA;AAIS;AAGR;AAAiB;AAAhB;AACiB;AACC;AACV;AAEP;AAAC;AAAA;AACW;AACD;AAID;AAMa;AAGV;AAAA;AACb;AAAA;AACF;AAAA;AAEA;AAIF;AAAgB;AAAf;AACW;AAKI;AAMR;AAAA;AAEN;AAGJ;AAAA;AAhFW;AAqFjB;AAII;AAAA;AAAC;AAAA;AACW;AACI;AAGZ;AAA8D;AAC3D;AACL;AAAA;AACF;AACC;AAGH;AAGN;AAE0C;AAGxC;AAAC;AAAA;AACW;AACO;AACE;AACqC;AACxD;AACwB;AACxB;AACc;AACH;AACe;AACP;AACd;AACL;AACe;AACf;AAAA;AACF;AAAA;AAAA;AAGN;AAGN;;"}
|
|
@@ -10,6 +10,7 @@ import { BellCrossedIcon } from '../icons/BellCrossed.js';
|
|
|
10
10
|
import { CheckCircleIcon } from '../icons/CheckCircle.js';
|
|
11
11
|
import { CheckCircleFillIcon } from '../icons/CheckCircleFill.js';
|
|
12
12
|
import { useOverrides } from '../overrides.js';
|
|
13
|
+
import { commentsResourceForVisibility } from '../shared.js';
|
|
13
14
|
import { cn } from '../utils/cn.js';
|
|
14
15
|
import { useStableComponent } from '../utils/use-stable-component.js';
|
|
15
16
|
import { useIntersectionCallback } from '../utils/use-visible.js';
|
|
@@ -165,7 +166,7 @@ const Thread = forwardRef(
|
|
|
165
166
|
}, [unreadIndex]);
|
|
166
167
|
const canComment = useHasPermissionAccess(
|
|
167
168
|
thread.roomId,
|
|
168
|
-
|
|
169
|
+
commentsResourceForVisibility(thread.visibility),
|
|
169
170
|
"write"
|
|
170
171
|
);
|
|
171
172
|
const stopPropagation = useCallback((event) => {
|
|
@@ -262,6 +263,7 @@ const Thread = forwardRef(
|
|
|
262
263
|
className: "lb-thread-comment",
|
|
263
264
|
"data-unread": isUnread ? "" : void 0,
|
|
264
265
|
comment,
|
|
266
|
+
visibility: thread.visibility,
|
|
265
267
|
indentContent: indentCommentContent,
|
|
266
268
|
showDeleted: showDeletedComments,
|
|
267
269
|
showActions,
|
|
@@ -332,6 +334,7 @@ const Thread = forwardRef(
|
|
|
332
334
|
{
|
|
333
335
|
className: "lb-thread-composer",
|
|
334
336
|
threadId: thread.id,
|
|
337
|
+
visibility: thread.visibility,
|
|
335
338
|
defaultCollapsed: showComposer === "collapsed" ? true : void 0,
|
|
336
339
|
showAttachments,
|
|
337
340
|
showFormattingControls: showComposerFormattingControls,
|