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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions moon/apps/web/components/MrView/MRComment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import { NotePlusIcon } from '@gitmono/ui/Icons'
import type { MenuProps } from 'antd'
import { Card, Dropdown } from 'antd/lib'
import { formatDistance, fromUnixTime } from 'date-fns'

import { getMarkdownExtensions } from '@gitmono/editor'
import { useDeleteIssueComment } from '@/hooks/issues/useDeleteIssueComment'
import { useDeleteMrCommentDelete } from '@/hooks/useDeleteMrCommentDelete'
import { Conversation } from '@/pages/[org]/mr/[id]'

import LexicalContent from './rich-editor/LexicalContent'

import { RichTextRenderer } from '@/components/RichTextRenderer'
import { useMemo } from 'react'
interface CommentProps {
conv: Conversation
id: string
Expand Down Expand Up @@ -64,6 +63,7 @@ const Comment = ({ conv, id, whoamI }: CommentProps) => {
}

const time = formatDistance(fromUnixTime(conv.created_at), new Date(), { addSuffix: true })
const extensions = useMemo(() => getMarkdownExtensions({ linkUnfurl: {} }), [])

return (
<Card
Expand All @@ -76,7 +76,9 @@ const Comment = ({ conv, id, whoamI }: CommentProps) => {
</Dropdown>
}
>
<LexicalContent lexicalJson={conv.comment} />
<div className='prose'>
<RichTextRenderer content={conv.comment} extensions={extensions} />
</div>
</Card>
)
}
Expand Down
190 changes: 190 additions & 0 deletions moon/apps/web/components/SimpleNoteEditor/SimpleNoteContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import {
DragEvent,
forwardRef,
KeyboardEvent,
memo,
MouseEvent,
useImperativeHandle,
useRef,
useState
} from 'react'
import { Editor as TTEditor } from '@tiptap/core'
import { EditorContent } from '@tiptap/react'

import { ActiveEditorComment, BlurAtTopOptions } from '@gitmono/editor'
import { LayeredHotkeys } from '@gitmono/ui'

import { AttachmentLightbox } from '@/components/AttachmentLightbox'
import { MentionList } from '@/components/MarkdownEditor/MentionList'
import { ReactionList } from '@/components/MarkdownEditor/ReactionList'
import { ResourceMentionList } from '@/components/MarkdownEditor/ResourceMentionList'
import { ADD_ATTACHMENT_SHORTCUT, SlashCommand } from '@/components/Post/Notes/SlashCommand'
import { useAutoScroll } from '@/hooks/useAutoScroll'
import { EMPTY_HTML } from '@/atoms/markdown'
import { CodeBlockLanguagePicker } from '@/components/CodeBlockLanguagePicker'
import { EditorBubbleMenu } from '@/components/EditorBubbleMenu'
import { MentionInteractivity } from '@/components/InlinePost/MemberHovercard'
import { DropProps, useEditorFileHandlers } from '@/components/MarkdownEditor/useEditorFileHandlers'
import { HighlightCommentPopover } from '@/components/NoteComments/HighlightCommentPopover'
import { useUploadNoteAttachments } from '@/components/Post/Notes/Attachments/useUploadAttachments'
import { NoteCommentPreview } from '@/components/Post/Notes/CommentRenderer'
import { useSimpleNoteEditor } from '@/components/SimpleNoteEditor/useSimpleNoteEditor'

interface Props {
commentId: string
editable?: 'all' | 'viewer'
autofocus?: boolean
content: string
onBlurAtTop?: BlurAtTopOptions['onBlur']
onKeyDown?: (event: KeyboardEvent) => void
}

export interface SimpleNoteContentRef {
focus(pos: 'start' | 'end' | 'restore' | 'start-newline' | MouseEvent): void
handleDrop(props: DropProps): void
handleDragOver(isOver: boolean, event: DragEvent): void
editor: TTEditor | null
clearAndBlur(): void
}

export const SimpleNoteContent = memo(
forwardRef<SimpleNoteContentRef, Props>((props, ref) => {
const { commentId, editable = 'viewer', autofocus = false, onBlurAtTop, content } = props

const [activeComment, setActiveComment] = useState<ActiveEditorComment | null>(null)
const [hoverComment, setHoverComment] = useState<ActiveEditorComment | null>(null)
const [openAttachmentId, setOpenAttachmentId] = useState<string | undefined>()

const canUploadAttachments = editable === 'all'
const upload = useUploadNoteAttachments({ noteId: commentId, enabled: canUploadAttachments })

const editor = useSimpleNoteEditor({
content,
autofocus,
editable: editable,
onHoverComment: setHoverComment,
onActiveComment: setActiveComment,
onOpenAttachment: setOpenAttachmentId,
onBlurAtTop
})

const { onDrop, onPaste, imperativeHandlers } = useEditorFileHandlers({
enabled: canUploadAttachments,
upload,
editor
})

// these functions allow us to call editorRef?.current?.handleDrop() etc. on the parent container
useImperativeHandle(
ref,
() => ({
clearAndBlur: () => editor.chain().setContent(EMPTY_HTML).blur().run(),
focus: (pos) => {
if (pos === 'start') {
editor.commands.focus('start')
} else if (pos === 'start-newline') {
editor.commands.focus('start')
editor.commands.insertContent('\n')
} else if (pos === 'end') {
editor.commands.focus('end')
} else if (pos === 'restore') {
editor.commands.focus()
} else if ('clientX' in pos && 'clientY' in pos && 'target' in pos) {
if (editor.view.dom.contains(pos.target as Node)) {
return
}

const { left, right, top } = editor.view.dom.getBoundingClientRect()
const isRight = pos.clientX > right
const editorPos = editor.view.posAtCoords({
left: isRight ? right : left,
top: pos.clientY
})

if (editorPos) {
const posAdjustment = isRight && editor.view.coordsAtPos(editorPos.pos).left === left ? -1 : 0

editor.commands.focus(editorPos.pos + posAdjustment)
} else if (pos.clientY < top) {
editor.commands.focus('start')
} else {
editor.commands.focus('end')
}
}
},
...imperativeHandlers,
editor
}),
[editor, imperativeHandlers]
)

const containerRef = useRef<HTMLDivElement>(null)

useAutoScroll({
ref: containerRef,
enabled: true
})

return (
<div ref={containerRef} className="relative min-h-[160px]">
<LayeredHotkeys
keys={ADD_ATTACHMENT_SHORTCUT}
callback={() => {
if (!editor.isFocused) return

const input = document.createElement('input')

input.type = 'file'
input.onchange = async () => {
if (input.files?.length) {
upload({
files: Array.from(input.files),
editor
})
}
}
input.click()
}}
options={{ enableOnContentEditable: true, enableOnFormTags: true }}
/>

<NoteCommentPreview
onExpand={() => {
if (hoverComment) {
setHoverComment(null)
setActiveComment(hoverComment)
}
}}
previewComment={activeComment ? null : hoverComment}
editor={editor}
noteId={commentId}
/>
<MentionInteractivity container={containerRef} />
<CodeBlockLanguagePicker editor={editor} />
<SlashCommand editor={editor} upload={upload} />
<MentionList editor={editor} />
<ResourceMentionList editor={editor} />
<ReactionList editor={editor} />

<AttachmentLightbox
selectedAttachmentId={openAttachmentId}
onClose={() => setOpenAttachmentId(undefined)}
onSelectAttachment={({ id }) => setOpenAttachmentId(id)}
/>

<HighlightCommentPopover
activeComment={activeComment}
editor={editor}
noteId={commentId}
onCommentDeactivated={() => setActiveComment(null)}
/>

<EditorBubbleMenu editor={editor} canComment />

<EditorContent editor={editor} onKeyDown={props.onKeyDown} onPaste={onPaste} onDrop={onDrop} />
</div>
)
})
)

SimpleNoteContent.displayName = 'SimpleNoteContent'
134 changes: 134 additions & 0 deletions moon/apps/web/components/SimpleNoteEditor/useSimpleNoteEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { useEffect, useMemo } from 'react'
import { EditorOptions, ReactNodeViewRenderer, useEditor } from '@tiptap/react'

import { ActiveEditorComment, BlurAtTopOptions, getNoteExtensions, PostNoteAttachmentOptions } from '@gitmono/editor'

import { InlineResourceMentionRenderer } from '@/components/InlineResourceMentionRenderer'
import { useControlClickLink } from '@/components/MarkdownEditor/ControlClickLink'
import { MediaGalleryRenderer } from '@/components/Post/MediaGalleryRenderer'
import { InlineRelativeTimeRenderer } from '@/components/RichTextRenderer/handlers/RelativeTime'
import { useCurrentUserOrOrganizationHasFeature } from '@/hooks/useCurrentUserOrOrganizationHasFeature'
import { notEmpty } from '@/utils/notEmpty'

import { LinkUnfurlRenderer } from '@/components/Post/LinkUnfurlRenderer'
import { NoteAttachmentRenderer } from '@/components/Post/Notes/Attachments/NoteAttachmentRenderer'
import { DragAndDrop } from '@/components/Post/Notes/DragAndDrop'

interface SimpleNoteEditorOptions {
content: string
onOpenAttachment?: PostNoteAttachmentOptions['onOpenAttachment']
autofocus?: boolean
editable?: 'all' | 'viewer' | 'none'
editorProps?: EditorOptions['editorProps']
onHoverComment?(comment: ActiveEditorComment | null): void
onActiveComment?(comment: ActiveEditorComment | null): void
onBlurAtTop?: BlurAtTopOptions['onBlur']
}

export function useSimpleNoteEditor({
content,
autofocus,
editable,
editorProps,
onHoverComment,
onActiveComment,
onOpenAttachment,
onBlurAtTop,
}: SimpleNoteEditorOptions) {
const linkOptions = useControlClickLink()
const hasRelativeTime = useCurrentUserOrOrganizationHasFeature('relative_time')

const extensions = useMemo(() => {
return [
...getNoteExtensions({
history: {
enabled: true
},
dropcursor: {
class: 'text-blue-500',
width: 2
},
link: linkOptions,
linkUnfurl: {
addNodeView() {
return ReactNodeViewRenderer(LinkUnfurlRenderer)
}
},
taskItem: {
canEdit() {
return editable !== 'none'
},
onReadOnlyChecked() {
return editable === 'viewer'
}
},
postNoteAttachment: {
onOpenAttachment,
addNodeView() {
return ReactNodeViewRenderer(NoteAttachmentRenderer)
}
},
mediaGallery: {
onOpenAttachment,
addNodeView() {
return ReactNodeViewRenderer(MediaGalleryRenderer)
}
},
resourceMention: {
addNodeView() {
return ReactNodeViewRenderer(InlineResourceMentionRenderer, { contentDOMElementTag: 'span' })
}
},
comment: {
enabled: true,
onCommentHovered: onHoverComment,
onCommentActivated: onActiveComment
},
codeBlockHighlighted: {
highlight: true
},
blurAtTop: {
enabled: !!onBlurAtTop,
onBlur: onBlurAtTop
},
relativeTime: {
disabled: !hasRelativeTime,
addNodeView() {
return ReactNodeViewRenderer(InlineRelativeTimeRenderer, { contentDOMElementTag: 'span' })
}
}
}),
DragAndDrop
].filter(notEmpty)
}, [editable, linkOptions, onActiveComment, onBlurAtTop, onHoverComment, onOpenAttachment, hasRelativeTime])

const allEditable = editable === 'all'

const editor = useEditor(
{
immediatelyRender: true,
shouldRerenderOnTransaction: false,
editorProps: {
attributes: {
class:
'new-posts prose select-text focus:outline-none w-full relative note min-w-full px-4',
style: "overflow-anchor: ''"
},
...editorProps
},
extensions,
autofocus: !!autofocus,
content,
editable: allEditable
},
[extensions]
)

useEffect(() => {
if (editor.isEditable !== allEditable) {
editor.setEditable(allEditable)
}
}, [editor, allEditable])

return editor
}
Loading