Răsfoiți Sursa

Neo: comments polish (empty placeholder, Cmd/Ctrl+Enter, manage menu icons, modal backdrop drag) (#5846)

* Neo: show the empty comments placeholder on first load

The first-load flag lived in a ref, so an empty thread never re-rendered
its footer and the placeholder only appeared after adding and deleting a
comment. Keep it in state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Neo: send comments and timeline posts with Cmd/Ctrl+Enter

The messenger keeps sending on Enter. Comments and the timeline post form
now send on Cmd/Ctrl+Enter and keep plain Enter as a newline; other forms
are unchanged. comments.submit_comment_on_enter now defaults to false.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Neo: show icons in the comment manage menu

The menu items dropped icon and image on the way to the dropdown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Neo: don't close a modal when a drag ends on the backdrop

Selecting text past the panel edge fired a click on the backdrop and asked
to discard unsaved changes. Close on a backdrop click only when the
pointerdown also landed outside the panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Neo: address review on modal backdrop and Cmd/Ctrl+Enter

- Enter/Space on the focused backdrop closes the modal again; only pointer
  presses need to start outside the panel.
- Cmd/Ctrl+Shift/Option+Enter stays a newline instead of sending.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
RomanL 1 săptămână în urmă
părinte
comite
80f9b18a49

+ 6 - 1
neo/packages/app/components/elements/comments-browse.js

@@ -111,6 +111,9 @@ export function CommentsBrowse({
         isFirstLoad: true,
         prevItems: []
     })
+    // State, not the ref: the empty placeholder must re-render once the first
+    // page lands, even when the list stays empty.
+    const [isLoaded, setIsLoaded] = useState(false)
 
     const baseParams = {
         //static
@@ -330,6 +333,7 @@ export function CommentsBrowse({
             dispatch({ type: 'SET_ITEMS', items })
             refetchRef.current.prevItems = items
             refetchRef.current.isFirstLoad = false
+            setIsLoaded(true)
             return
         }
 
@@ -446,7 +450,7 @@ export function CommentsBrowse({
             <View>
                 {(browseParams.object_id && hasNextPage && isFetchingNextPage) ? (
                     <View className=''><Loading /></View>
-                ) : (browseParams.object_id && !refetchState.visibleItems.length && !refetchRef.current.isFirstLoad ? appStatic('components_comments_empty') : null)}
+                ) : (browseParams.object_id && !refetchState.visibleItems.length && isLoaded ? appStatic('components_comments_empty') : null)}
                 {/* Spacer so last comments clear sticky composer (+ keyboard on native). */}
                 {marginBottom > 0 ? <View style={{ height: marginBottom }} collapsable={false} /> : null}
             </View>
@@ -456,6 +460,7 @@ export function CommentsBrowse({
         hasNextPage,
         isFetchingNextPage,
         refetchState.visibleItems.length,
+        isLoaded,
         marginBottom,
         dismissComposerIfIdle,
     ]);

+ 2 - 1
neo/packages/app/components/forms/comments.js

@@ -164,8 +164,9 @@ export default function FormComments(props) {
                                         initialHeight: minHeightEditor,
                                         maxHeight: maxHeightEditor,
                                         onFocus: setIsFocus,
+                                        // Cmd/Ctrl+Enter submits; plain Enter is a newline
+                                        // unless `comments.submit_comment_on_enter` is on.
                                         onEnterSubmit: handleEditorEnterSubmit,
-                                        enableSubmitOnEnter: isDesktop,
                                     }
                                 )}
                             </View>

+ 8 - 0
neo/packages/app/components/forms/feed.js

@@ -314,6 +314,12 @@ export default function FormFeed({ data, handleSubmit, exProps, name, response }
     const isPollsPresent = !!data.inputs['polls'];
     const isLabelsPresent = !!data.inputs['labels'];
     const isButtonDisabled = !hasText || ((!isHiddenVisibility && object_privacy_view == '')) ? true : false;
+
+    // Same guards as the submit button: nothing to post or a post already in flight.
+    const handleEditorEnterSubmit = () => {
+        if (isButtonDisabled || formContext.formState.isSubmitting) return;
+        handleSubmit();
+    };
     if (isFormOnly) {
         return (
             <RNView style={{ position: 'relative', width: '100%', flex: 1, gap: 12 }}>
@@ -388,6 +394,8 @@ export default function FormFeed({ data, handleSubmit, exProps, name, response }
                                         classes: 'flex-auto',
                                         initialHeight: 160,
                                         maxHeight: screenHeight / 2 - 80 ,
+                                        // Cmd/Ctrl+Enter posts; plain Enter is a newline.
+                                        onEnterSubmit: handleEditorEnterSubmit,
                                     }
                                 )}
                             </View>

+ 3 - 8
neo/packages/app/components/units/comments.js

@@ -547,6 +547,8 @@ const MenuManage_ = memo(
                     name: aItem.name,
                     link: aItem.link,
                     title: sTitle,
+                    icon: aItem.icon,
+                    image: aItem.image,
                 }
             })
             : []
@@ -557,14 +559,7 @@ const MenuManage_ = memo(
                     <DropdownMenu
                         mode="popup"
                         defaultOpen={defaultOpen}
-                        items={aMenuManageItems.map((aItem) => {
-                            return {
-                                id: aItem.id ? aItem.id : aItem.name,
-                                name: aItem.name,
-                                link: aItem.link,
-                                title: aItem.title,
-                            }
-                        })}
+                        items={aMenuManageItems}
                         onSelect={handleManageMenuSelect}
                         buttonProps={{
                             style: 'borderless',

+ 26 - 2
neo/packages/app/design/controls/modal.tsx

@@ -115,6 +115,9 @@ export function Modal({
     const closeRef = useRef<any>(null);
     const scrollRef = useRef<any>(null);
     const scrollYRef = useRef(0);
+    const panelRef = useRef<any>(null);
+    // Web: whether the last pointerdown landed on the fog, outside the panel.
+    const pressStartedOutsideRef = useRef(false);
 
     const handleScroll = useCallback((event: any) => {
         const y = event?.nativeEvent?.contentOffset?.y;
@@ -236,8 +239,29 @@ export function Modal({
     const modalBottomOffset = 24 + (insets?.bottom ?? 0)
 
 
+    // A drag that starts inside the panel and ends on the fog (selecting text past
+    // the edge) still fires a click on the fog. Close only when the press began
+    // on the fog too.
+    useEffect(() => {
+        const isShown = onVisible == null ? true : !!onVisible;
+        if (!isWeb || !isShown) return undefined;
+
+        const onPointerDown = (event: PointerEvent) => {
+            const target = event.target as Node | null;
+            const fog = fogRef.current;
+            const panel = panelRef.current;
+            pressStartedOutsideRef.current = !!(target && fog?.contains(target) && !panel?.contains(target));
+        };
+
+        document.addEventListener('pointerdown', onPointerDown, true);
+        return () => document.removeEventListener('pointerdown', onPointerDown, true);
+    }, [onVisible]);
+
     const handleWebOuterPress = useCallback((event: any) => {
-        if (isOuterClose) {
+        // Enter/Space on the focused backdrop has no pointerdown to check. RNW passes
+        // the raw keyup event for keys and a synthetic click event for pointers.
+        const fromKeyboard = String((event?.nativeEvent ?? event)?.type || '').startsWith('key');
+        if (isOuterClose && (fromKeyboard || pressStartedOutsideRef.current)) {
             tryClose();
         }
         event.stopPropagation();
@@ -416,7 +440,7 @@ export function Modal({
                     onPress={handleWebOuterPress}
                 >
                     <RemoveScroll className={`flex-1  flex flex-col w-full sm:justify-center ${autoHeight && 'justify-center'} overflow-hidden`} >
-                        <View style={{paddingBottom: isKeyboardOpen ? 96 : 0, height: (isDesktop && !isIPadWeb) || autoHeight ? 'auto' : heightActual }} className={`w-full ${maxWidth} left-0 right-0 z-50 w-full mx-auto ${modalSettings.container} `}>
+                        <View ref={panelRef} style={{paddingBottom: isKeyboardOpen ? 96 : 0, height: (isDesktop && !isIPadWeb) || autoHeight ? 'auto' : heightActual }} className={`w-full ${maxWidth} left-0 right-0 z-50 w-full mx-auto ${modalSettings.container} `}>
                        
                             {content}
 

+ 8 - 2
neo/packages/app/lib/editor/comment-editor-keyboard.ts

@@ -1,12 +1,18 @@
 /**
- * Comment composer Enter-key policy (web / DOM KeyboardEvent).
- * Shift/Option+Enter: newline; plain Enter and Cmd/Ctrl+Enter: submit.
+ * Composer Enter-key policy (web / DOM KeyboardEvent).
+ * Submit on Enter (messenger): Shift/Option+Enter is a newline, plain Enter submits.
+ * Submit on Cmd/Ctrl+Enter (comments, feed posts): plain Enter stays a newline.
  */
 
 export function isCommentEditorNewlineEnter(event: any) {
     return !!(event?.shiftKey || event?.altKey);
 }
 
+/** Cmd/Ctrl+Enter; with Shift/Option added it stays a newline. */
+export function isSubmitModEnter(event: any) {
+    return !!(event?.metaKey || event?.ctrlKey) && !isCommentEditorNewlineEnter(event);
+}
+
 export function isCommentEditorEnterKey(event: any) {
     const key = event?.key ?? event?.code;
     return key === 'Enter' || key === 'LineFeed';

+ 21 - 5
neo/packages/app/lib/editor/use-enriched-keyboard.ts

@@ -1,7 +1,7 @@
 'use client'
 import { useCallback, useEffect, useRef } from 'react'
 import { Platform } from 'react-native'
-import { isCommentEditorEnterKey, isCommentEditorNewlineEnter } from 'app/lib/editor/comment-editor-keyboard'
+import { isCommentEditorEnterKey, isCommentEditorNewlineEnter, isSubmitModEnter } from 'app/lib/editor/comment-editor-keyboard'
 import { deleteWholeMention } from 'app/lib/editor/editor-mention-shared'
 
 const isWeb = Platform.OS === 'web'
@@ -36,8 +36,11 @@ function handleMentionNavKey(key: string, h: any, insertMention: any) {
  * Enter/mention key routing for the enriched editor.
  *
  * Comment composers run the library in `newline` mode and route Enter
- * themselves: Shift/Option+Enter inserts a break, plain Enter submits, and
- * while the mention list is open Enter/arrows/Escape drive the list.
+ * themselves: with `submitOnEnter` (messenger) Shift/Option+Enter inserts a
+ * break and plain Enter submits. Any editor with `onEnterSubmit` but without
+ * `submitOnEnter` (comments, feed posts) submits on Cmd/Ctrl+Enter and keeps
+ * plain Enter as a newline. While the mention list is open Enter/arrows/Escape
+ * drive the list.
  *
  * - native: via the library's `onKeyPress` (returned here);
  * - web: via a capture-phase keydown on the ProseMirror root so TipTap's own
@@ -57,11 +60,18 @@ export function useEnrichedKeyboard({
     }, [onEnterSubmit])
 
     const routesEnter = submitOnEnter && isCommentsEditor
+    const routesModEnter = !submitOnEnter && !!onEnterSubmit
 
     const onKeyPress = useCallback((e: any) => {
         const nativeEvent = e?.nativeEvent
         const h = handlersRef.current
 
+        if (routesModEnter && isCommentEditorEnterKey(nativeEvent) && isSubmitModEnter(nativeEvent)
+            && !h?.suggestions?.length) {
+            onEnterSubmitRef.current?.()
+            return
+        }
+
         if (routesEnter && isCommentEditorEnterKey(nativeEvent)) {
             if (isCommentEditorNewlineEnter(nativeEvent)) return
             if (h?.suggestions?.length) {
@@ -73,7 +83,7 @@ export function useEnrichedKeyboard({
         }
 
         handleMentionNavKey(nativeEvent?.key, h, insertMention)
-    }, [routesEnter, handlersRef, insertMention])
+    }, [routesEnter, routesModEnter, handlersRef, insertMention])
 
     useEffect(() => {
         if (!isWeb || !tipTap?.view?.dom) return undefined
@@ -98,6 +108,12 @@ export function useEnrichedKeyboard({
                 return
             }
 
+            if (routesModEnter && isCommentEditorEnterKey(event) && isSubmitModEnter(event)) {
+                stopKey(event)
+                onEnterSubmitRef.current?.()
+                return
+            }
+
             if (!routesEnter || !isCommentEditorEnterKey(event)) return
             stopKey(event)
             if (isCommentEditorNewlineEnter(event)) {
@@ -109,7 +125,7 @@ export function useEnrichedKeyboard({
 
         dom.addEventListener('keydown', onKeyDownCapture, true)
         return () => dom.removeEventListener('keydown', onKeyDownCapture, true)
-    }, [tipTap, routesEnter, handlersRef, insertMention])
+    }, [tipTap, routesEnter, routesModEnter, handlersRef, insertMention])
 
     return isWeb ? undefined : onKeyPress
 }

+ 1 - 1
neo/packages/app/settings/elements.js

@@ -54,7 +54,7 @@ export const settingsElements = {
         count_in_feed: 3, //OLD appSetting('layout', 'comments_count_in_feed')
         mentions: true, //OLD appSetting('layout', 'comments_mentions')
         in_reply: true, //OLD appSetting('layout', 'show_in_reply_comments')
-        submit_comment_on_enter: true,
+        submit_comment_on_enter: false, // false: Cmd/Ctrl+Enter submits, Enter is a newline
     },
     carousel: {
         image_width: '', //appSetting('layout', 'carousel_image_width')