Repository navigation
Feat/add link handling in comments #1675
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
da92dd4
f76dace
b9398f4
9983b1a
722fa12
7d3f272
156ad0b
e973401
527e4ba
031e90b
de5ef12
16b3a5f
6561371
f0a1cc5
3037d49
82b76a3
917b849
e112898
6296130
469b00f
9dd4293
16afcc0
cae9d28
490b1e3
ef0c7e5
396a8e8
c9aa384
bdc4013
a3aeead
1a2d0f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -67,6 +67,7 @@ | |||
| "@upstash/ratelimit": "^2.0.8", | ||||
| "@upstash/redis": "^1.37.0", | ||||
| "@vercel/analytics": "^2.0.1", | ||||
| "@vercel/og": "^0.9.0", | ||||
|
||||
| "@vercel/og": "^0.9.0", |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,6 @@ | ||||||||||||||||||
| 'use client'; | ||||||||||||||||||
|
|
||||||||||||||||||
| import * as Popover from '@radix-ui/react-popover'; | ||||||||||||||||||
| import * as React from 'react'; | ||||||||||||||||||
| import { useState } from 'react'; | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -8,6 +9,8 @@ import { useCreateComment } from '~/core/hooks/use-create-comment'; | |||||||||||||||||
| import { usePersonalSpaceId } from '~/core/hooks/use-personal-space-id'; | ||||||||||||||||||
| import { useSpace } from '~/core/hooks/use-space'; | ||||||||||||||||||
|
|
||||||||||||||||||
| import { SelectEntity } from '~/design-system/select-entity'; | ||||||||||||||||||
|
|
||||||||||||||||||
| import { EntityVoteButtons } from '~/partials/entity-page/entity-vote-buttons'; | ||||||||||||||||||
|
|
||||||||||||||||||
| import { NavUtils } from '~/core/utils/utils'; | ||||||||||||||||||
|
|
@@ -88,6 +91,7 @@ export function CommentSection({ entityId, spaceId }: CommentSectionProps) { | |||||||||||||||||
| <TopLevelCommentInput | ||||||||||||||||||
| onSubmit={text => handleCreateComment(text)} | ||||||||||||||||||
| isCreating={isCreating} | ||||||||||||||||||
| spaceId={spaceId} | ||||||||||||||||||
| /> | ||||||||||||||||||
| {totalCount > 0 && ( | ||||||||||||||||||
| <> | ||||||||||||||||||
|
|
@@ -180,9 +184,11 @@ function CommentFilters({ | |||||||||||||||||
| function TopLevelCommentInput({ | ||||||||||||||||||
| onSubmit, | ||||||||||||||||||
| isCreating, | ||||||||||||||||||
| spaceId, | ||||||||||||||||||
| }: { | ||||||||||||||||||
| onSubmit: (text: string) => void; | ||||||||||||||||||
| isCreating: boolean; | ||||||||||||||||||
| spaceId: string; | ||||||||||||||||||
| }) { | ||||||||||||||||||
| const [isExpanded, setIsExpanded] = useState(false); | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -207,6 +213,7 @@ function TopLevelCommentInput({ | |||||||||||||||||
| placeholder="" | ||||||||||||||||||
| autoFocus | ||||||||||||||||||
| onCancel={() => setIsExpanded(false)} | ||||||||||||||||||
| spaceId={spaceId} | ||||||||||||||||||
| /> | ||||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
@@ -218,15 +225,19 @@ function CommentInput({ | |||||||||||||||||
| autoFocus = false, | ||||||||||||||||||
| onCancel, | ||||||||||||||||||
| initialValue = '', | ||||||||||||||||||
| spaceId, | ||||||||||||||||||
| }: { | ||||||||||||||||||
| onSubmit: (text: string) => void; | ||||||||||||||||||
| isCreating: boolean; | ||||||||||||||||||
| placeholder: string; | ||||||||||||||||||
| autoFocus?: boolean; | ||||||||||||||||||
| onCancel?: () => void; | ||||||||||||||||||
| initialValue?: string; | ||||||||||||||||||
| spaceId: string; | ||||||||||||||||||
| }) { | ||||||||||||||||||
| const [text, setText] = useState(initialValue); | ||||||||||||||||||
| const [isMentioning, setIsMentioning] = useState(false); | ||||||||||||||||||
| const [mentionStartIndex, setMentionStartIndex] = useState<number | null>(null); | ||||||||||||||||||
| const textareaRef = React.useRef<HTMLTextAreaElement>(null); | ||||||||||||||||||
|
|
||||||||||||||||||
| const handleSubmit = () => { | ||||||||||||||||||
|
|
@@ -240,10 +251,64 @@ function CommentInput({ | |||||||||||||||||
| if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { | ||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||
| handleSubmit(); | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
| if (e.key === 'Escape' && onCancel) { | ||||||||||||||||||
| onCancel(); | ||||||||||||||||||
| if (e.key === 'Escape') { | ||||||||||||||||||
| if (isMentioning) { | ||||||||||||||||||
| setIsMentioning(false); | ||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
| if (onCancel) { | ||||||||||||||||||
| onCancel(); | ||||||||||||||||||
| } | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if (e.key === '@') { | ||||||||||||||||||
| const { selectionStart } = e.currentTarget as HTMLTextAreaElement; | ||||||||||||||||||
| // Trigger mention only if @ is at start or preceded by whitespace | ||||||||||||||||||
| const beforeChar = text[selectionStart - 1]; | ||||||||||||||||||
| if (!beforeChar || /\s/.test(beforeChar)) { | ||||||||||||||||||
| const textarea = e.currentTarget as HTMLTextAreaElement; | ||||||||||||||||||
| // Small timeout to ensure the '@' is inserted into the textarea first | ||||||||||||||||||
| // and doesn't leak into the SelectEntity search input which auto-focuses. | ||||||||||||||||||
| setTimeout(() => { | ||||||||||||||||||
| const coords = getCaretCoordinates(textarea, selectionStart + 1); | ||||||||||||||||||
| setMentionCoords(coords); | ||||||||||||||||||
| setMentionStartIndex(selectionStart); | ||||||||||||||||||
| setIsMentioning(true); | ||||||||||||||||||
| }, 0); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| const onMentionSelect = (result: { id: string; name: string | null }) => { | ||||||||||||||||||
| if (mentionStartIndex === null) return; | ||||||||||||||||||
|
|
||||||||||||||||||
| const before = text.slice(0, mentionStartIndex); | ||||||||||||||||||
| const after = text.slice(mentionStartIndex); | ||||||||||||||||||
|
|
||||||||||||||||||
| // We only replace the '@' and any characters that might have been typed since | ||||||||||||||||||
| // However, SelectEntity has its own input, so we just replace from mentionStartIndex | ||||||||||||||||||
| // But we need to find how many characters were typed after @ in the textarea if we were tracking query there | ||||||||||||||||||
| // For now, let's assume we replace just the '@' and any non-whitespace chars immediately after it at that position | ||||||||||||||||||
| const match = after.match(/^@\S*/); | ||||||||||||||||||
| const lengthToReplace = match ? match[0].length : 1; | ||||||||||||||||||
|
|
||||||||||||||||||
| const newText = before + `[${result.name || result.id}](graph://${result.id})` + after.slice(lengthToReplace); | ||||||||||||||||||
| setText(newText); | ||||||||||||||||||
|
Comment on lines
+296
to
+300
|
||||||||||||||||||
| setIsMentioning(false); | ||||||||||||||||||
| setMentionStartIndex(null); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Focus back and set cursor | ||||||||||||||||||
| setTimeout(() => { | ||||||||||||||||||
| if (textareaRef.current) { | ||||||||||||||||||
| textareaRef.current.focus(); | ||||||||||||||||||
| const newCursorPos = before.length + `[${result.name || result.id}](graph://${result.id})`.length; | ||||||||||||||||||
| textareaRef.current.setSelectionRange(newCursorPos, newCursorPos); | ||||||||||||||||||
| } | ||||||||||||||||||
| }, 0); | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| // Auto-resize textarea | ||||||||||||||||||
|
|
@@ -257,18 +322,56 @@ function CommentInput({ | |||||||||||||||||
|
|
||||||||||||||||||
| const hasText = text.trim().length > 0; | ||||||||||||||||||
|
|
||||||||||||||||||
| const [mentionCoords, setMentionCoords] = useState({ x: 0, y: 0 }); | ||||||||||||||||||
|
|
||||||||||||||||||
| return ( | ||||||||||||||||||
| <div className="flex flex-col gap-2 rounded-lg border border-grey-02 p-3"> | ||||||||||||||||||
| <textarea | ||||||||||||||||||
| ref={textareaRef} | ||||||||||||||||||
| value={text} | ||||||||||||||||||
| onChange={e => setText(e.target.value)} | ||||||||||||||||||
| onKeyDown={handleKeyDown} | ||||||||||||||||||
| placeholder={placeholder} | ||||||||||||||||||
| autoFocus={autoFocus} | ||||||||||||||||||
| rows={3} | ||||||||||||||||||
| className="w-full resize-none bg-transparent text-body text-text outline-none placeholder:text-grey-04" | ||||||||||||||||||
| /> | ||||||||||||||||||
| <Popover.Root open={isMentioning} onOpenChange={setIsMentioning}> | ||||||||||||||||||
| {/* Invisible fixed anchor positioned at the caret */} | ||||||||||||||||||
| <div | ||||||||||||||||||
| style={{ | ||||||||||||||||||
| position: 'fixed', | ||||||||||||||||||
| left: mentionCoords.x, | ||||||||||||||||||
| top: mentionCoords.y, | ||||||||||||||||||
| width: 0, | ||||||||||||||||||
| height: 0, | ||||||||||||||||||
| pointerEvents: 'none', | ||||||||||||||||||
| zIndex: 9999, | ||||||||||||||||||
| }} | ||||||||||||||||||
| > | ||||||||||||||||||
| <Popover.Anchor /> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| <textarea | ||||||||||||||||||
| ref={textareaRef} | ||||||||||||||||||
| value={text} | ||||||||||||||||||
| onChange={e => setText(e.target.value)} | ||||||||||||||||||
| onKeyDown={handleKeyDown} | ||||||||||||||||||
| placeholder={placeholder} | ||||||||||||||||||
| autoFocus={autoFocus} | ||||||||||||||||||
| rows={3} | ||||||||||||||||||
| className="relative w-full resize-none bg-transparent text-body text-text outline-none placeholder:text-grey-04" | ||||||||||||||||||
| /> | ||||||||||||||||||
| <Popover.Portal> | ||||||||||||||||||
| <Popover.Content | ||||||||||||||||||
| side="bottom" | ||||||||||||||||||
| align="start" | ||||||||||||||||||
| sideOffset={0} | ||||||||||||||||||
| className="z-9999 ml-2 -mt-2" | ||||||||||||||||||
| onOpenAutoFocus={e => { | ||||||||||||||||||
| // We want SelectEntity to focus, but we don't want Radix to do its own focus management | ||||||||||||||||||
| // because it might conflict with our timing. However, SelectEntity already auto-focuses. | ||||||||||||||||||
|
Comment on lines
+360
to
+362
|
||||||||||||||||||
| onOpenAutoFocus={e => { | |
| // We want SelectEntity to focus, but we don't want Radix to do its own focus management | |
| // because it might conflict with our timing. However, SelectEntity already auto-focuses. | |
| onOpenAutoFocus={event => { | |
| // We want SelectEntity to focus, but we don't want Radix to do its own focus management | |
| // because it might conflict with our timing. However, SelectEntity already auto-focuses. | |
| event.preventDefault(); | |
| event.stopPropagation(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
showUrlWarning?: booleanwas added toSelectEntityProps, but the component doesn’t destructure or use this prop anywhere, so it has no effect and may confuse callers. Either wire it through to the relevant UI/behavior (and include it in the component’s parameter destructuring) or remove the prop until it’s implemented.