Skip to content

Commit 8295d10

Browse files
committed
4000 char limit on comments
1 parent d5a80ea commit 8295d10

6 files changed

Lines changed: 71 additions & 2 deletions

File tree

app/actions/post.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ func (action *AddNewComment) Validate(ctx context.Context, user *entity.User) *v
221221

222222
if action.Content == "" {
223223
result.AddFieldFailure("content", propertyIsRequired(ctx, "comment"))
224+
} else if len(action.Content) > 4000 {
225+
result.AddFieldFailure("content", propertyMaxStringLen(ctx, "comment", 4000))
224226
}
225227

226228
messages, err := validate.MultiImageUpload(ctx, nil, action.Attachments, validate.MultiImageUploadOpts{
@@ -346,6 +348,8 @@ func (action *EditComment) Validate(ctx context.Context, user *entity.User) *val
346348

347349
if action.Content == "" {
348350
result.AddFieldFailure("content", propertyIsRequired(ctx, "comment"))
351+
} else if len(action.Content) > 2000 {
352+
result.AddFieldFailure("content", propertyMaxStringLen(ctx, "comment", 2000))
349353
}
350354

351355
if len(action.Attachments) > 0 {

app/actions/post_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package actions_test
22

33
import (
44
"context"
5+
"strings"
56
"testing"
67

78
"github.com/getfider/fider/app"
@@ -133,3 +134,27 @@ func TestDeleteComment(t *testing.T) {
133134
authorized = action.IsAuthorized(context.Background(), administrator)
134135
Expect(authorized).IsTrue()
135136
}
137+
138+
func TestAddNewComment_TooLongContent(t *testing.T) {
139+
RegisterT(t)
140+
141+
action := &actions.AddNewComment{Content: strings.Repeat("a", 4001)}
142+
result := action.Validate(context.Background(), nil)
143+
ExpectFailed(result, "content")
144+
}
145+
146+
func TestAddNewComment_AtMaxLength(t *testing.T) {
147+
RegisterT(t)
148+
149+
action := &actions.AddNewComment{Content: strings.Repeat("a", 4000)}
150+
result := action.Validate(context.Background(), nil)
151+
ExpectSuccess(result)
152+
}
153+
154+
func TestEditComment_TooLongContent(t *testing.T) {
155+
RegisterT(t)
156+
157+
action := &actions.EditComment{Content: strings.Repeat("a", 4001)}
158+
result := action.Validate(context.Background(), nil)
159+
ExpectFailed(result, "content")
160+
}

public/components/common/form/CommentEditor.scss

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@
2424
width: 100%;
2525
}
2626

27+
.c-comment-editor-counter {
28+
font-size: 0.75rem;
29+
color: var(--colors-gray-500);
30+
text-align: right;
31+
margin-top: spacing(1);
32+
33+
&--over {
34+
color: var(--colors-red-600);
35+
font-weight: 600;
36+
}
37+
}
38+
2739
.c-editor-button-group {
2840
display: flex;
2941
flex-wrap: wrap;

public/components/common/form/CommentEditor.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,14 +237,19 @@ interface CommentEditorProps {
237237
onGetImageSrc?: (bkey: string) => string
238238
maxAttachments?: number
239239
maxImageSizeKB?: number
240+
maxLength?: number
240241
}
241242

243+
const DEFAULT_MAX_LENGTH = 4000
244+
242245
const Tiptap: React.FunctionComponent<CommentEditorProps> = (props) => {
246+
const maxLength = props.maxLength ?? DEFAULT_MAX_LENGTH
243247
const [isRawMarkdownMode, setIsRawMarkdownMode] = useState(false)
244248
const [imageUploads, setImageUploads] = useState<ImageUpload[]>([])
245249
const [isLinkModalOpen, setIsLinkModalOpen] = useState(false)
246250
const [selectedText, setSelectedText] = useState("")
247251
const [markdownText, setMarkdownText] = useState(props.initialValue ?? "")
252+
const [contentLength, setContentLength] = useState((props.initialValue ?? "").length)
248253
const allowedProtocols = useAllowedProtocols()
249254

250255
// Use a ref instead of state for tracking document images
@@ -326,6 +331,8 @@ const Tiptap: React.FunctionComponent<CommentEditorProps> = (props) => {
326331
// Get the current markdown content
327332
const markdown = isRawMarkdownMode ? editor.getText() : editor.storage.markdown.getMarkdown()
328333

334+
setContentLength(markdown.length)
335+
329336
// Pass markdown to parent (plain text will be generated in ShareFeedback if needed)
330337
props.onChange && props.onChange(markdown)
331338

@@ -604,6 +611,7 @@ const Tiptap: React.FunctionComponent<CommentEditorProps> = (props) => {
604611
const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
605612
const newValue = e.target.value
606613
setMarkdownText(newValue)
614+
setContentLength(newValue.length)
607615

608616
// Notify parent component
609617
if (props.onChange) {
@@ -639,13 +647,25 @@ const Tiptap: React.FunctionComponent<CommentEditorProps> = (props) => {
639647
onChange={handleTextareaChange}
640648
onFocus={props.onFocus}
641649
disabled={props.disabled}
650+
maxLength={maxLength}
642651
placeholder={props.placeholder ?? "Write your comment here..."}
643652
data-testid="markdown-textarea"
644653
/>
645654
) : (
646655
<EditorContent editor={editor} data-testid="tiptap-editor" />
647656
)}
648657
</div>
658+
{contentLength >= maxLength * 0.8 && (
659+
<div
660+
className={classSet({
661+
"c-comment-editor-counter": true,
662+
"c-comment-editor-counter--over": contentLength > maxLength,
663+
})}
664+
data-testid="comment-editor-counter"
665+
>
666+
{contentLength} / {maxLength}
667+
</div>
668+
)}
649669
<DisplayError fields={[props.field]} error={ctx.error} />
650670
<LinkInsertModal isOpen={isLinkModalOpen} onClose={() => setIsLinkModalOpen(false)} onInsertLink={handleInsertLink} selectedText={selectedText} />
651671
</div>

public/pages/ShowPost/components/CommentInput.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@ export const CommentInput = (props: CommentInputProps) => {
2727
return cache.session.get(getCacheKey(CACHE_TITLE_KEY))
2828
}
2929

30+
const COMMENT_MAX_LENGTH = 4000
31+
3032
const fider = useFider()
3133
const [isSignInModalOpen, setIsSignInModalOpen] = useState(false)
3234
const [error, setError] = useState<Failure | undefined>(undefined)
3335
const [isClient, setIsClient] = useState(false)
36+
const [contentLength, setContentLength] = useState((cache.session.get(`${CACHE_TITLE_KEY}${props.post.id}`) ?? "").length)
3437

3538
// Use the attachments hook
3639
const { attachments, handleImageUploaded, getImageSrc, clearAttachments } = useAttachments({
@@ -74,8 +77,11 @@ export const CommentInput = (props: CommentInputProps) => {
7477

7578
const commentChanged = useCallback((value: string): void => {
7679
cache.session.set(getCacheKey(CACHE_TITLE_KEY), value)
80+
setContentLength(value.length)
7781
}, [])
7882

83+
const isOverLimit = contentLength > COMMENT_MAX_LENGTH
84+
7985
return (
8086
<>
8187
<SignInModal isOpen={isSignInModalOpen} onClose={hideModal} />
@@ -94,13 +100,14 @@ export const CommentInput = (props: CommentInputProps) => {
94100
placeholder={i18n._({ id: "showpost.commentinput.placeholder", message: "Leave a comment" })}
95101
maxAttachments={2}
96102
maxImageSizeKB={5 * 1024}
103+
maxLength={COMMENT_MAX_LENGTH}
97104
onGetImageSrc={getImageSrc}
98105
onImageUploaded={handleImageUploaded}
99106
/>
100107

101108
{hasContent && (
102109
<>
103-
<Button disabled={!fider.session.isAuthenticated} variant="primary" onClick={submit} className="mt-4">
110+
<Button disabled={!fider.session.isAuthenticated || isOverLimit} variant="primary" onClick={submit} className="mt-4">
104111
<Trans id="action.postcomment">Post</Trans>
105112
</Button>
106113
</>

public/pages/ShowPost/components/ShowComment.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,12 @@ export const ShowComment = (props: ShowCommentProps) => {
227227
placeholder={comment.content}
228228
maxAttachments={2}
229229
maxImageSizeKB={5 * 1024}
230+
maxLength={4000}
230231
onGetImageSrc={getImageSrc}
231232
onImageUploaded={handleImageUploaded}
232233
/>
233234
<div className="mt-2">
234-
<Button size="small" onClick={saveEdit} variant="primary">
235+
<Button size="small" onClick={saveEdit} variant="primary" disabled={newContent.length > 2000}>
235236
<Trans id="action.save">Save</Trans>
236237
</Button>
237238
<Button variant="tertiary" size="small" onClick={cancelEdit}>

0 commit comments

Comments
 (0)