From b6f62971eb0bbee2a9ab0b22a952ba8de1ac7e09 Mon Sep 17 00:00:00 2001 From: Komediruzecki Date: Sat, 8 Jan 2022 16:09:13 +0100 Subject: [PATCH 1/7] Add initial overhaul of comment system Add comment list instead of threads Add back and forth buttons Update comment author name Remove statuses for threads Remove filters for threads/comments Add logic for placeholders and creating new comment/thread Add logic for comment replies Beautify comment system Add indent for replies Slack like comment on thread, remove action item Reduce size of user icons Change comment and thread handling and style Remove context menu and use popup actions Update thread and comment styling Remove comment handling for markdown preview Update style and design for comment thread and list Update edit of comments Update some style for comments --- .../components/Comments/CommentInput.tsx | 41 ++- src/cloud/components/Comments/CommentList.tsx | 113 +++--- .../components/Comments/CommentManager.tsx | 150 ++++---- .../Comments/ThreadActionButton.tsx | 11 +- src/cloud/components/Comments/ThreadItem.tsx | 322 +++++++++++++----- src/cloud/components/Comments/ThreadList.tsx | 13 +- src/cloud/components/Editor/index.tsx | 117 +------ .../CustomizedMarkdownPreviewer.tsx | 9 - src/cloud/components/MarkdownView/index.tsx | 59 ---- src/cloud/lib/hooks/useCommentManagerState.ts | 4 +- src/cloud/lib/hooks/useThreadMenuActions.tsx | 51 +-- src/cloud/lib/i18n/enUS.ts | 2 +- src/mobile/components/pages/DocViewPage.tsx | 112 ++---- 13 files changed, 433 insertions(+), 571 deletions(-) diff --git a/src/cloud/components/Comments/CommentInput.tsx b/src/cloud/components/Comments/CommentInput.tsx index b017bfe08b..320212e8f5 100644 --- a/src/cloud/components/Comments/CommentInput.tsx +++ b/src/cloud/components/Comments/CommentInput.tsx @@ -20,6 +20,7 @@ interface CommentInputProps { value?: string autoFocus?: boolean users: SerializedUser[] + placeholder: string } const smallUserIconStyle = { width: '20px', height: '20px', lineHeight: '17px' } @@ -28,8 +29,10 @@ export function CommentInput({ value = '', autoFocus = false, users, + placeholder, }: CommentInputProps) { const [working, setWorking] = useState(false) + const [isInputEmpty, setIsInputEmpty] = useState(false) const inputRef = useRef(null) const { translate } = useI18n() const onSuggestionSelect = useRef((item: SerializedUser, hint: string) => { @@ -76,7 +79,9 @@ export function CommentInput({ if (value.length > 0) { inputRef.current.appendChild(toFragment(value)) } else { - resetInitialContent(inputRef.current) + // todo: [komediruzecki-2022-01-8] see why this was done... - makes placeholder impossible with pseudo:class + // maybe we could leave this and add placeholder directly, but then we need to remove it on type start... + // resetInitialContent(inputRef.current) } if (autoFocus) { inputRef.current.focus() @@ -99,11 +104,24 @@ export function CommentInput({ } }, [onSubmit]) + const onKeyUp = useCallback(() => { + const inputContent = + inputRef.current !== null ? fromNode(inputRef.current).trim() : '' + setIsInputEmpty(inputContent === '') + }, []) + const onKeyDown: React.KeyboardEventHandler = useCallback( (ev) => { onKeyDownListener(ev) - if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) { + const inputContent = + inputRef.current !== null ? fromNode(inputRef.current).trim() : '' + // setIsInputEmpty(inputContent === '') + if ( + ev.key === 'Enter' && + (ev.ctrlKey || ev.metaKey) && + inputContent !== '' + ) { ev.preventDefault() ev.stopPropagation() submit() @@ -150,19 +168,28 @@ export function CommentInput({ } }, []) + const onCommentInput = useCallback(() => { + if (inputRef.current != null) { + setIsInputEmpty(fromNode(inputRef.current).trim() === '') + } + }, []) + return (
+ onInput={onCommentInput} + data-placeholder={placeholder} + /> - @@ -207,6 +234,12 @@ const InputContainer = styled.div` color: ${({ theme }) => theme.colors.text.primary}; padding: 5px 10px; margin-bottom: ${({ theme }) => theme.sizes.spaces.df}px; + + border-radius: ${({ theme }) => theme.borders.radius}px; + + &:empty:before { + content: attr(data-placeholder); + } } & .comment__input__suggestions { diff --git a/src/cloud/components/Comments/CommentList.tsx b/src/cloud/components/Comments/CommentList.tsx index 038ff11334..de0ae68335 100644 --- a/src/cloud/components/Comments/CommentList.tsx +++ b/src/cloud/components/Comments/CommentList.tsx @@ -4,12 +4,8 @@ import styled from '../../../design/lib/styled' import UserIcon from '../UserIcon' import { format } from 'date-fns' import Icon from '../../../design/components/atoms/Icon' -import { mdiDotsVertical, mdiClose } from '@mdi/js' +import { mdiClose, mdiPencil, mdiTrashCanOutline } from '@mdi/js' import { SerializedUser } from '../../interfaces/db/user' -import { - useContextMenu, - MenuTypes, -} from '../../../design/lib/stores/contextMenu' import CommentInput from './CommentInput' import sortBy from 'ramda/es/sortBy' import prop from 'ramda/es/prop' @@ -18,6 +14,7 @@ import { toText } from '../../lib/comments' interface CommentThreadProps { comments: Comment[] className: string + commentItemClassName: string updateComment: (comment: Comment, message: string) => Promise deleteComment: (comment: Comment) => Promise user?: SerializedUser @@ -27,6 +24,7 @@ interface CommentThreadProps { function CommentList({ comments, className, + commentItemClassName, updateComment, deleteComment, user, @@ -39,14 +37,15 @@ function CommentList({ return (
{sorted.map((comment) => ( - +
+ +
))}
) @@ -60,7 +59,9 @@ interface CommentItemProps { users: SerializedUser[] } -const smallUserIconStyle = { width: '32px', height: '32px', lineHeight: '28px' } +const smallUserIconStyle = { width: '28px', height: '28px', lineHeight: '22px' } + +// todo: [komediruzecki-2022-01-18] Num of replies not reflect upon removal of item! export function CommentItem({ comment, editable, @@ -69,25 +70,7 @@ export function CommentItem({ users, }: CommentItemProps) { const [editing, setEditing] = useState(false) - const { popup } = useContextMenu() - - const openContextMenu: React.MouseEventHandler = useCallback( - (event) => { - popup(event, [ - { - type: MenuTypes.Normal, - label: 'Edit', - onClick: () => setEditing(true), - }, - { - type: MenuTypes.Normal, - label: 'Delete', - onClick: () => deleteComment(comment), - }, - ]) - }, - [popup, comment, deleteComment] - ) + const [showingContextMenu, setShowingContextMenu] = useState(false) const submitComment = useCallback( async (message: string) => { @@ -106,13 +89,17 @@ export function CommentItem({
{' '}
-
+
setShowingContextMenu(true)} + onMouseLeave={() => setShowingContextMenu(false)} + >
{comment.user.displayName} - {format(comment.createdAt, 'do MMMM hh:mmaaa')} + {format(comment.createdAt, 'hh:mmaaa MMM do')} {editable && (editing ? ( @@ -120,13 +107,27 @@ export function CommentItem({
) : ( -
- -
+ showingContextMenu && ( +
+
setEditing(true)} + className='comment__meta__actions__edit' + > + +
+
deleteComment(comment)} + className='comment__meta__actions__remove' + > + +
+
+ ) ))}
{editing ? ( theme.sizes.spaces.xsm}px; - margin-right: ${({ theme }) => theme.sizes.spaces.df}px; + width: 39px; } .comment__content { @@ -156,13 +155,13 @@ const CommentItemContainer = styled.div` .comment__meta { display: flex; align-items: center; - margin-bottom: ${({ theme }) => theme.sizes.spaces.sm}px; + margin-bottom: 4px; & svg { - color: ${({ theme }) => theme.colors.icon.default} + color: ${({ theme }) => theme.colors.icon.default}; &:hover { - color: ${({ theme }) => theme.colors.icon.active} + color: ${({ theme }) => theme.colors.icon.active}; } } } @@ -191,6 +190,32 @@ const CommentItemContainer = styled.div` white-space: pre-wrap; word-break: break-word; } + + .comment__meta__actions { + display: flex; + flex-direction: row; + justify-self: flex-end; + align-self: center; + position: absolute; + right: 7px; + + padding: 4px; + border-radius: ${({ theme }) => theme.borders.radius}px; + + background-color: #1e2024; + + .comment__meta__actions__edit, + .comment__meta__actions__remove { + height: 20px; + + color: ${({ theme }) => theme.colors.text.subtle}; + + &:hover { + cursor: pointer; + color: ${({ theme }) => theme.colors.text.primary}; + } + } + } ` export default CommentList diff --git a/src/cloud/components/Comments/CommentManager.tsx b/src/cloud/components/Comments/CommentManager.tsx index 2f5e3c2c56..ae1ee3c017 100644 --- a/src/cloud/components/Comments/CommentManager.tsx +++ b/src/cloud/components/Comments/CommentManager.tsx @@ -1,22 +1,17 @@ -import React, { useMemo, useState } from 'react' +import React, { useMemo } from 'react' import { Thread, Comment } from '../../interfaces/db/comments' import Spinner from '../../../design/components/atoms/Spinner' -import { mdiPlusBoxOutline, mdiArrowLeft } from '@mdi/js' +import { mdiArrowLeft } from '@mdi/js' import Icon from '../../../design/components/atoms/Icon' import CommentList from './CommentList' import styled from '../../../design/lib/styled' import CommentInput from './CommentInput' -import ThreadActionButton from './ThreadActionButton' -import Button from '../../../design/components/atoms/Button' import { CreateThreadRequestBody } from '../../api/comments/thread' import { SerializedUser } from '../../interfaces/db/user' import ThreadList from './ThreadList' -import ThreadStatusFilterControl, { - StatusFilter, -} from '../ThreadStatusFilterControl' -import { partitionOnStatus } from '../../../design/lib/utils/comments' import { useI18n } from '../../lib/hooks/useI18n' import { lngKeys } from '../../lib/i18n/types' +import Button from '../../../design/components/atoms/Button' export type State = | { mode: 'list_loading'; thread?: { id: string } } @@ -43,8 +38,6 @@ export interface Actions { createThread: ( data: Omit ) => Promise - reopenThread: (thread: Thread) => Promise - closeThread: (thread: Thread) => Promise deleteThread: (thread: Thread) => Promise threadOutdated: (thread: Thread) => Promise createComment: (thread: Thread, message: string) => Promise @@ -62,8 +55,6 @@ function CommentManager({ state, setMode, createThread, - reopenThread, - closeThread, deleteThread, createComment, updateComment, @@ -72,20 +63,6 @@ function CommentManager({ users, }: CommentManagerProps) { const { translate } = useI18n() - const [statusFilter, setStatusFitler] = useState('open') - const partitioned = useMemo(() => { - return partitionOnStatus(state.mode === 'list_loading' ? [] : state.threads) - }, [state]) - - const counts = useMemo(() => { - return { - all: state.mode === 'list_loading' ? 0 : state.threads.length, - open: partitioned.open.length, - closed: partitioned.closed.length, - outdated: partitioned.outdated.length, - } - }, [partitioned, state]) - const usersOrEmpty = useMemo(() => { return users != null ? users : [] }, [users]) @@ -100,59 +77,59 @@ function CommentManager({
) case 'list': { - const stateThreads = - statusFilter === 'all' ? state.threads : partitioned[statusFilter] - const threads = - state.filter != null - ? stateThreads.filter(state.filter) - : stateThreads return ( - <> +
setMode({ mode: 'thread', thread })} - onOpen={reopenThread} - onClose={closeThread} onDelete={deleteThread} + users={usersOrEmpty} + updateComment={updateComment} /> -
setMode({ mode: 'new_thread' })} - > - {' '} - {translate(lngKeys.ThreadCreate)} +
+ { + await createThread({ comment }) + }} + autoFocus={true} + users={usersOrEmpty} + />
- +
) } case 'thread': { return (
+ +
{state.thread.context}
- {state.thread.status.type === 'open' && ( - createComment(state.thread, message)} - autoFocus={true} - users={usersOrEmpty} - /> - )} - {state.thread.status.type === 'closed' && ( - - )} + createComment(state.thread, message)} + autoFocus={true} + users={usersOrEmpty} + />
) @@ -160,8 +137,9 @@ function CommentManager({ case 'new_thread': { return (
-
{state.data.context}
+ {/*
{state.data.context}
*/} { await createThread({ ...state.data, comment }) }} @@ -173,11 +151,8 @@ function CommentManager({ } } }, [ - translate, state, createThread, - reopenThread, - closeThread, deleteThread, createComment, updateComment, @@ -185,37 +160,12 @@ function CommentManager({ setMode, user, usersOrEmpty, - statusFilter, - partitioned, ]) return (
- {(state.mode !== 'list' || state.filter != null) && ( -
setMode({ mode: 'list' })} - > - -
- )}

{translate(lngKeys.ThreadsTitle)}

- {state.mode === 'list' && ( - - )} - {state.mode === 'thread' && ( - - )}
{content}
@@ -225,12 +175,12 @@ function CommentManager({ const Container = styled.div` margin: auto; height: 100vh; - width: 350px; + width: 480px; display: flex; flex-direction: column; border-left: 1px solid ${({ theme }) => theme.colors.border.main}; - border-radius: 0px; - background-color: ${({ theme }) => theme.colors.background.secondary}; + border-radius: 0; + background-color: ${({ theme }) => theme.colors.background.primary}; color: ${({ theme }) => theme.colors.text.primary}; font-size: ${({ theme }) => theme.sizes.fonts.md}px; position: relative; @@ -240,7 +190,7 @@ const Container = styled.div` } .header { - padding: 0px ${({ theme }) => theme.sizes.spaces.df}px; + padding: 0 ${({ theme }) => theme.sizes.spaces.df}px; & h4 { margin: 0; } @@ -269,13 +219,20 @@ const Container = styled.div` display: flex; flex-direction: column-reverse; scrollbar-width: thin; - padding: 0px ${({ theme }) => theme.sizes.spaces.df}px; + padding: 0 ${({ theme }) => theme.sizes.spaces.df}px; margin-bottom: ${({ theme }) => theme.sizes.spaces.df}px; + + margin-left: ${({ theme }) => theme.sizes.spaces.sm}px; + & .comment__list { & > div { margin-bottom: ${({ theme }) => theme.sizes.spaces.df}px; } + & .comment__list__comment__item:not(:first-child) { + margin-left: 35px; + } + &:hover { .comment__meta__menu { display: block; @@ -316,6 +273,15 @@ const Container = styled.div` left: 50%; transform: translate3d(-50%, -50%, 0); } + + .thread__list__container { + display: flex; + flex-direction: column; + flex: 1; + .thread__list__container__create__thread { + margin-top: auto; + } + } ` export default CommentManager diff --git a/src/cloud/components/Comments/ThreadActionButton.tsx b/src/cloud/components/Comments/ThreadActionButton.tsx index 9c4925853d..9dc2d5df38 100644 --- a/src/cloud/components/Comments/ThreadActionButton.tsx +++ b/src/cloud/components/Comments/ThreadActionButton.tsx @@ -15,19 +15,12 @@ import Flexbox from '../../../design/components/atoms/Flexbox' interface ThreadActionButtonProps { thread: Thread - onClose: (thread: Thread) => any - onOpen: (thread: Thread) => any onDelete: (thread: Thread) => any } -function ThreadActionButton({ - thread, - onClose, - onOpen, - onDelete, -}: ThreadActionButtonProps) { +function ThreadActionButton({ thread, onDelete }: ThreadActionButtonProps) { const { popup } = useContextMenu() - const actions = useThreadActions({ thread, onClose, onOpen, onDelete }) + const actions = useThreadActions({ thread, onDelete }) const { getThreadStatusLabel } = useI18n() const openActionMenu: React.MouseEventHandler = useCallback( diff --git a/src/cloud/components/Comments/ThreadItem.tsx b/src/cloud/components/Comments/ThreadItem.tsx index 10089be334..49c16b4fa4 100644 --- a/src/cloud/components/Comments/ThreadItem.tsx +++ b/src/cloud/components/Comments/ThreadItem.tsx @@ -1,79 +1,182 @@ -import React, { useCallback } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { isToday, format, formatDistanceToNow } from 'date-fns' -import { Thread } from '../../interfaces/db/comments' -import { - mdiAlertCircleOutline, - mdiDotsVertical, - mdiAlertCircleCheckOutline, -} from '@mdi/js' +import { Thread, Comment } from '../../interfaces/db/comments' import UserIcon from '../UserIcon' -import Icon from '../../../design/components/atoms/Icon' import styled from '../../../design/lib/styled' -import useThreadActions, { - ThreadActionProps, -} from '../../lib/hooks/useThreadMenuActions' -import { useContextMenu } from '../../../design/lib/stores/contextMenu' +import { ThreadActionProps } from '../../lib/hooks/useThreadMenuActions' import { lngKeys } from '../../lib/i18n/types' import { useI18n } from '../../lib/hooks/useI18n' +import { listThreadComments } from '../../api/comments/comment' +import { SerializedUser } from '../../interfaces/db/user' +import { + mdiClose, + mdiMessageReplyTextOutline, + mdiPencil, + mdiTrashCanOutline, +} from '@mdi/js' +import Icon from '../../../design/components/atoms/Icon' +import CommentInput from './CommentInput' export type ThreadListItemProps = ThreadActionProps & { onSelect: (thread: Thread) => void + users: SerializedUser[] + updateComment: (comment: Comment, message: string) => Promise } -const smallUserIconStyle = { width: '22px', height: '22px', lineHeight: '18px' } -function ThreadItem({ thread, onSelect, ...rest }: ThreadListItemProps) { - const actions = useThreadActions({ thread, ...rest }) - const { popup } = useContextMenu() +const smallUserIconStyle = { width: '28px', height: '28px', lineHeight: '22px' } +const smallerUserIconReplyStyle = { + width: '22px', + height: '22px', + lineHeight: '18px', +} + +function ThreadItem({ + thread, + onSelect, + onDelete, + updateComment, + users, +}: ThreadListItemProps) { const { translate } = useI18n() + const [editing, setEditing] = useState(false) + + const [threadComments, setThreadComments] = useState(null) + const [showingContextMenu, setShowingContextMenu] = useState(false) - const openActionMenu: React.MouseEventHandler = useCallback( - (event) => { - event.preventDefault() - event.stopPropagation() - popup(event, actions) + const reloadComments = useCallback(() => { + listThreadComments({ id: thread.id }).then((comments) => { + setThreadComments(comments) + }) + }, [thread.id]) + + useEffect(() => { + reloadComments() + }, [reloadComments, thread.id]) + + const showReplyForm = useCallback(() => { + if (threadComments == null || threadComments.length > 1) { + return + } + + setShowingContextMenu(true) + }, [threadComments]) + + const hideReplyForm = useCallback(() => { + setShowingContextMenu(false) + }, []) + + const submitComment = useCallback( + async (message: string) => { + if (threadComments == null || threadComments.length == 0) { + return + } + await updateComment(threadComments[0], message) + setEditing(false) + reloadComments() }, - [actions, popup] + [reloadComments, threadComments, updateComment] + ) + + const onCommentDelete = useCallback( + (thread) => { + onDelete(thread) + reloadComments() + }, + [onDelete, reloadComments] ) return ( - onSelect(thread)}> -
-
- -
- {thread.selection != null - ? thread.context - : translate(lngKeys.ThreadFullDocLabel)} + +
+
+
+ {thread.contributors.map((user) => ( + + ))}
+ {threadComments && threadComments.length > 0 && ( +
+ {thread.contributors[0].displayName} + + {formatDate(thread.lastCommentTime)} + + {editing ? ( + + ) : ( +
+ {threadComments[0].message} +
+ )} + + {threadComments && threadComments.length > 1 && ( +
+ {thread.contributors.map((user) => ( + + ))} +
onSelect(thread)} + className={'thread__comment__line__replies__link'} + > + {translate(lngKeys.ThreadReplies, { + count: thread.commentCount, + })} +
+ + {formatDate(thread.lastCommentTime)} + +
+ )} +
+ )}
-
- -
-
-
-
- {thread.contributors.map((user) => ( - - ))} - {translate(lngKeys.ThreadReplies, { count: thread.commentCount })} - - {formatDate(thread.lastCommentTime)} - -
+ {editing ? ( +
setEditing(false)}> + +
+ ) : ( + showingContextMenu && ( +
+
onSelect(thread)} + className='comment__meta__actions__comment' + > + +
+
setEditing(true)} + className='comment__meta__actions__edit' + > + +
+
onCommentDelete(thread)} + className='comment__meta__actions__remove' + > + +
+
+ ) + )}
) @@ -81,16 +184,19 @@ function ThreadItem({ thread, onSelect, ...rest }: ThreadListItemProps) { const StyledListItem = styled.div` padding: ${({ theme }) => theme.sizes.spaces.df}px 0; - border-bottom: 1px solid ${({ theme }) => theme.colors.border.main}; cursor: default; - .thread__info__line__date { - color: ${({ theme }) => theme.colors.text.subtle}; - font-size: ${({ theme }) => theme.sizes.fonts.sm}px; - padding-left: 4px; + + .thread { + display: flex; + flex-direction: row; + + justify-content: space-between; + position: relative; } - &:hover .thread__action { - opacity: 1; + .thread__info { + display: flex; + width: 100%; } & .thread__row { @@ -102,44 +208,76 @@ const StyledListItem = styled.div` & .thread__info__line { display: flex; - align-items: baseline; - overflow: hidden; + + .thread__info__line__icon { + width: 39px; + } + & > * { - margin-right: ${({ theme }) => theme.sizes.spaces.xsm}px; + margin-right: ${({ theme }) => theme.sizes.spaces.sm}px; } - & > .thread__item__context { - margin: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - &.thread__item__context--highlighted { - color: white; - background-color: #705400; + } + + & .thread__comment__line { + width: 100%; + align-items: center; + + & .thread__comment__line__replies__link { + margin-left: ${({ theme }) => theme.sizes.spaces.sm}px; + margin-top: ${({ theme }) => theme.sizes.spaces.xsm}px; + align-self: center; + color: #519aba; + + &:hover { + color: #65afd0; } } - } - & .thread__status { - flex-shrink: 0; - &.thread__status--open { - color: ${({ theme }) => theme.colors.variants.success.base}; + & .thread__comment__line__icon { + width: 39px; + margin-top: ${({ theme }) => theme.sizes.spaces.xsm}px; } - &.thread__status--closed { - color: ${({ theme }) => theme.colors.variants.danger.base}; + & .thread__comment__line__date { + align-self: center; + color: ${({ theme }) => theme.colors.text.subtle}; + font-size: ${({ theme }) => theme.sizes.fonts.sm}px; + padding-left: 4px; + margin-top: ${({ theme }) => theme.sizes.spaces.xsm}px; } - &.thread__status--outdated { - color: ${({ theme }) => theme.colors.icon.default}; + & .thread__comment__line_more_replies_container { + display: flex; + align-items: center; } } - & .thread__action { - height: 20px; - opacity: 0; - color: ${({ theme }) => theme.colors.text.subtle}; - &:hover { - color: ${({ theme }) => theme.colors.text.primary}; + + .comment__meta__actions { + position: absolute; + right: 12px; + top: -8px; + display: flex; + flex-direction: row; + justify-self: flex-start; + align-self: center; + + gap: 4px; + border-radius: ${({ theme }) => theme.borders.radius}px; + + background-color: #1e2024; + + .comment__meta__actions__comment, + .comment__meta__actions__edit, + .comment__meta__actions__remove { + height: 20px; + margin: 5px; + + color: ${({ theme }) => theme.colors.text.subtle}; + + &:hover { + cursor: pointer; + color: ${({ theme }) => theme.colors.text.primary}; + } } } ` diff --git a/src/cloud/components/Comments/ThreadList.tsx b/src/cloud/components/Comments/ThreadList.tsx index 350ba89a27..06db198b09 100644 --- a/src/cloud/components/Comments/ThreadList.tsx +++ b/src/cloud/components/Comments/ThreadList.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react' import ThreadItem, { ThreadListItemProps } from './ThreadItem' -import { Thread } from '../../interfaces/db/comments' +import { Comment, Thread } from '../../interfaces/db/comments' import { sortBy } from 'ramda' import { highlightComment, @@ -10,14 +10,15 @@ import styled from '../../../design/lib/styled' interface ThreadListProps extends Omit { threads: Thread[] + updateComment: (comment: Comment, message: string) => Promise } function ThreadList({ threads, onSelect, - onOpen, - onClose, onDelete, + users, + updateComment, }: ThreadListProps) { const sorted = useMemo(() => { return sortBy((thread) => thread.lastCommentTime, threads).reverse() @@ -35,9 +36,9 @@ function ThreadList({
))} @@ -47,7 +48,7 @@ function ThreadList({ const Container = styled.div` & > div { - padding: 0px ${({ theme }) => theme.sizes.spaces.df}px; + padding: 0 ${({ theme }) => theme.sizes.spaces.df}px; &:hover { background-color: ${({ theme }) => theme.colors.background.tertiary}; } diff --git a/src/cloud/components/Editor/index.tsx b/src/cloud/components/Editor/index.tsx index f01d1282ec..c548420923 100644 --- a/src/cloud/components/Editor/index.tsx +++ b/src/cloud/components/Editor/index.tsx @@ -10,11 +10,7 @@ import attachFileHandlerToCodeMirrorEditor, { OnFileCallback, } from '../../lib/editor/plugins/fileHandler' import { uploadFile, buildTeamFileUrl } from '../../api/teams/files' -import { - createRelativePositionFromTypeIndex, - createAbsolutePositionFromRelativePosition, - YEvent, -} from 'yjs' +import { YEvent } from 'yjs' import { useGlobalKeyDownHandler, preventKeyboardEventPropagation, @@ -43,7 +39,6 @@ import { mdiPencil, mdiEyeOutline, mdiViewSplitVertical, - mdiCommentTextOutline, mdiDotsHorizontal, } from '@mdi/js' import EditorToolButton from './EditorToolButton' @@ -66,7 +61,6 @@ import { } from '../../lib/utils/events' import { ScrollSync, scrollSyncer } from '../../lib/editor/scrollSync' import CodeMirrorEditor from '../../lib/editor/components/CodeMirrorEditor' -import { SelectionContext } from '../MarkdownView' import { usePage } from '../../lib/stores/pageStore' import { useToast } from '../../../design/lib/stores/toast' import { LoadingButton } from '../../../design/components/atoms/Button' @@ -78,7 +72,6 @@ import PresenceIcons from '../Topbar/PresenceIcons' import { TopbarControlProps } from '../../../design/components/organisms/Topbar' import CommentManager from '../Comments/CommentManager' import useCommentManagerState from '../../lib/hooks/useCommentManagerState' -import { HighlightRange } from '../../lib/rehypeHighlight' import { getDocLinkHref } from '../Link/DocLink' import throttle from 'lodash.throttle' import { useI18n } from '../../lib/hooks/useI18n' @@ -234,7 +227,6 @@ const Editor = ({ const previewRef = useRef(null) const syncScroll = useRef() const [scrollSync, setScrollSync] = useState(true) - const [viewComments, setViewComments] = useState([]) const { settings } = useSettings() const fontSize = settings['general.editorFontSize'] @@ -319,81 +311,6 @@ const Editor = ({ } }) - const newRangeThread = useCallback( - (selection: SelectionContext) => { - if (realtime == null) { - return - } - const text = realtime.doc.getText('content') - const anchor = createRelativePositionFromTypeIndex(text, selection.start) - const head = createRelativePositionFromTypeIndex(text, selection.end) - setPreferences({ docContextMode: 'comment' }) - commentActions.setMode({ - mode: 'new_thread', - context: selection.text, - selection: { - anchor, - head, - }, - }) - }, - [realtime, commentActions, setPreferences] - ) - - const calculatePositions = useCallback(() => { - if (commentState.mode === 'list_loading' || realtime == null) { - return - } - - const comments: HighlightRange[] = [] - for (const thread of commentState.threads) { - if (thread.selection != null && thread.status.type !== 'outdated') { - const absoluteAnchor = createAbsolutePositionFromRelativePosition( - thread.selection.anchor, - realtime.doc - ) - const absoluteHead = createAbsolutePositionFromRelativePosition( - thread.selection.head, - realtime.doc - ) - - if ( - absoluteAnchor != null && - absoluteHead != null && - absoluteAnchor.index !== absoluteHead.index - ) { - if (thread.status.type === 'open') { - comments.push({ - id: thread.id, - start: absoluteAnchor.index, - end: absoluteHead.index, - active: - commentState.mode === 'thread' && - thread.id === commentState.thread.id, - }) - } - } else if (connState === 'synced') { - commentActions.threadOutdated(thread) - } - } - } - setViewComments(comments) - }, [commentState, realtime, commentActions, connState]) - - const commentClick = useCallback( - (ids: string[]) => { - if (commentState.mode !== 'list_loading') { - const idSet = new Set(ids) - setPreferences({ docContextMode: 'comment' }) - commentActions.setMode({ - mode: 'list', - filter: (thread) => idSet.has(thread.id), - }) - } - }, - [commentState, commentActions, setPreferences] - ) - const changeEditorLayout = useCallback( (target: LayoutMode) => { setEditorLayout(target) @@ -855,18 +772,6 @@ const Editor = ({ } }, [focusEditorHeading]) - useEffect(() => { - if (realtime != null) { - realtime.doc.on('update', calculatePositions) - return () => realtime.doc.off('update', calculatePositions) - } - return undefined - }, [realtime, calculatePositions]) - - useEffect(() => { - calculatePositions() - }, [calculatePositions]) - useEffect(() => { if (!mountedRef.current) return if (docRef.current !== doc.id) { @@ -1159,15 +1064,6 @@ const Editor = ({ className='scroller' getEmbed={getEmbed} scrollerRef={previewRef} - comments={viewComments} - commentClick={commentClick} - SelectionMenu={({ selection }) => ( - -
newRangeThread(selection)}> - -
-
- )} /> @@ -1363,27 +1259,20 @@ const StyledPreview = styled.div` } ` -const StyledSelectionMenu = styled.div` - display: flex; - padding: 8px; - max-height: 37px; - cursor: pointer; -` - const StyledEditor = styled.div` display: flex; justify-content: center; flex-grow: 1; position: relative; top: 0; - bottom: 0px; + bottom: 0; width: 100%; height: auto; min-height: 0; font-size: 15px; &.preview, .preview { - ${rightSidePageLayout} + ${rightSidePageLayout}; margin: auto; } & .CodeMirrorWrapper { diff --git a/src/cloud/components/MarkdownView/CustomizedMarkdownPreviewer.tsx b/src/cloud/components/MarkdownView/CustomizedMarkdownPreviewer.tsx index d0a3f5ce55..e56042a796 100644 --- a/src/cloud/components/MarkdownView/CustomizedMarkdownPreviewer.tsx +++ b/src/cloud/components/MarkdownView/CustomizedMarkdownPreviewer.tsx @@ -2,7 +2,6 @@ import React from 'react' import MarkdownView, { SelectionState } from './index' import { usePreviewStyle } from '../../../lib/preview' import { EmbedDoc } from '../../lib/docEmbedPlugin' -import { HighlightRange } from '../../lib/rehypeHighlight' import { useSettings } from '../../lib/stores/settings' interface CustomizedMarkdownViewProps { @@ -20,8 +19,6 @@ interface CustomizedMarkdownViewProps { ) => Promise | EmbedDoc | undefined scrollerRef?: React.RefObject SelectionMenu?: React.ComponentType<{ selection: SelectionState['context'] }> - comments?: HighlightRange[] - commentClick?: (id: string[]) => void codeFence?: boolean previewStyle?: string } @@ -35,9 +32,6 @@ const CustomizedMarkdownPreviewer = ({ className, getEmbed, scrollerRef, - SelectionMenu, - comments, - commentClick, codeFence = true, }: CustomizedMarkdownViewProps) => { const { previewStyle } = usePreviewStyle() @@ -53,9 +47,6 @@ const CustomizedMarkdownPreviewer = ({ className={className} getEmbed={getEmbed} scrollerRef={scrollerRef} - SelectionMenu={SelectionMenu} - comments={comments} - commentClick={commentClick} codeFence={codeFence} previewStyle={previewStyle} codeBlockTheme={settings['general.codeBlockTheme']} diff --git a/src/cloud/components/MarkdownView/index.tsx b/src/cloud/components/MarkdownView/index.tsx index 01751d1165..3a84dfdc6d 100644 --- a/src/cloud/components/MarkdownView/index.tsx +++ b/src/cloud/components/MarkdownView/index.tsx @@ -35,11 +35,6 @@ import SelectionTooltip from './SelectionTooltip' import useSelectionLocation, { Rect, } from '../../lib/selection/useSelectionLocation' -import rehypeHighlight, { HighlightRange } from '../../lib/rehypeHighlight' -import rehypeGutters from '../../lib/rehypeGutters' -import { Node as UnistNode } from 'unist' -import { mdiCommentTextOutline } from '@mdi/js' -import Icon from '../../../design/components/atoms/Icon' import styled from '../../../design/lib/styled' import throttle from 'lodash.throttle' import CodeFence from '../../../design/components/atoms/markdown/CodeFence' @@ -117,8 +112,6 @@ interface MarkdownViewProps { ) => Promise | EmbedDoc | undefined scrollerRef?: React.RefObject SelectionMenu?: React.ComponentType<{ selection: SelectionState['context'] }> - comments?: HighlightRange[] - commentClick?: (id: string[]) => void codeFence?: boolean previewStyle?: string codeBlockTheme?: CodeMirrorEditorTheme @@ -134,8 +127,6 @@ const MarkdownView = ({ getEmbed, scrollerRef, SelectionMenu, - comments, - commentClick, codeFence = true, previewStyle, codeBlockTheme = 'default', @@ -250,19 +241,6 @@ const MarkdownView = ({ ) } : shortcodeHandler, - comment_count: (props: any) => { - return props.count != null && props.comments != null ? ( -
- commentClick && commentClick(props.comments.split(' ')) - } - > - {' '} - {props.count} -
- ) : null - }, }, } @@ -304,8 +282,6 @@ const MarkdownView = ({ theme: codeBlockTheme, }) .use(rehypeMermaid) - .use(rehypeHighlight, comments || []) - .use(rehypeGutters, makeCommentGutters(comments || [])) .use(rehypePosition) .use(rehypeReact, rehypeReactConfig) @@ -316,10 +292,8 @@ const MarkdownView = ({ headerLinks, getEmbed, codeBlockTheme, - comments, updateContent, content, - commentClick, ]) const processorRef = useRef(markdownProcessor) @@ -526,37 +500,4 @@ const StyledTooltipContent = styled.div` max-height: 50px; ` -function makeCommentGutters(highlights: HighlightRange[]) { - return (node: UnistNode): UnistNode | null => { - // todo: [komediruzecki-2021-11-20] End known to be null - if (node.position?.end == null) { - return null - } - const posStart = node.position?.start.offset - const posEnd = node.position?.end.offset - if (posStart != null && posEnd != null) { - const allHighlights = highlights.filter( - (highlight) => highlight.start >= posStart && highlight.start <= posEnd - ) - if (allHighlights.length > 0) { - return { - type: 'element', - tagName: 'div', - children: [ - { - type: 'element', - tagName: 'comment_count', - properties: { - count: allHighlights.length, - comments: allHighlights.map(({ id }) => id), - }, - }, - ], - } - } - } - return null - } -} - export default MarkdownView diff --git a/src/cloud/lib/hooks/useCommentManagerState.ts b/src/cloud/lib/hooks/useCommentManagerState.ts index 3a55fa2da1..d96d4c694b 100644 --- a/src/cloud/lib/hooks/useCommentManagerState.ts +++ b/src/cloud/lib/hooks/useCommentManagerState.ts @@ -1,5 +1,5 @@ import { useComments } from '../stores/comments' -import { Thread, Comment } from '../../../cloud/interfaces/db/comments' +import { Thread, Comment } from '../../interfaces/db/comments' import { useState, useEffect, useCallback, useMemo, useRef } from 'react' import { State, @@ -65,8 +65,6 @@ function useCommentManagerState(docId: string): [State, Actions] { return { setMode, createThread, - reopenThread: threadActions.reopen, - closeThread: threadActions.close, deleteThread: threadActions.delete, threadOutdated: threadActions.outdated, createComment: commentActions.create, diff --git a/src/cloud/lib/hooks/useThreadMenuActions.tsx b/src/cloud/lib/hooks/useThreadMenuActions.tsx index 740251f16b..1ba38d561c 100644 --- a/src/cloud/lib/hooks/useThreadMenuActions.tsx +++ b/src/cloud/lib/hooks/useThreadMenuActions.tsx @@ -1,29 +1,17 @@ import React, { useMemo } from 'react' import { Thread } from '../../interfaces/db/comments' import Icon from '../../../design/components/atoms/Icon' -import styled from '../../../design/lib/styled' import { MenuItem, MenuTypes } from '../../../design/lib/stores/contextMenu' -import { - mdiAlertCircleOutline, - mdiAlertCircleCheckOutline, - mdiTrashCanOutline, -} from '@mdi/js' +import { mdiTrashCanOutline } from '@mdi/js' import { useI18n } from './useI18n' import { lngKeys } from '../i18n/types' export interface ThreadActionProps { thread: Thread - onClose: (thread: Thread) => any - onOpen: (thread: Thread) => any onDelete: (thread: Thread) => any } -function useThreadActions({ - thread, - onClose, - onOpen, - onDelete, -}: ThreadActionProps) { +function useThreadActions({ thread, onDelete }: ThreadActionProps) { const { translate } = useI18n() const actions: MenuItem[] = useMemo(() => { const deleteAction: MenuItem = { @@ -33,40 +21,9 @@ function useThreadActions({ onClick: () => onDelete(thread), } - if (thread.status.type === 'outdated') { - return [deleteAction] - } - - return thread.status.type === 'closed' - ? [ - { - icon: , - type: MenuTypes.Normal, - label: translate(lngKeys.GeneralOpenVerb), - onClick: () => onOpen(thread), - }, - deleteAction, - ] - : [ - { - icon: , - type: MenuTypes.Normal, - label: translate(lngKeys.GeneralCloseVerb), - onClick: () => onClose(thread), - }, - deleteAction, - ] - }, [thread, onClose, onOpen, onDelete, translate]) - + return [deleteAction] + }, [onDelete, thread, translate]) return actions } -const SuccessIcon = styled(Icon)` - color: ${({ theme }) => theme.colors.variants.success.base}; -` - -const WarningIcon = styled(Icon)` - color: ${({ theme }) => theme.colors.variants.danger.base}; -` - export default useThreadActions diff --git a/src/cloud/lib/i18n/enUS.ts b/src/cloud/lib/i18n/enUS.ts index 0d15d7be84..7e81503060 100644 --- a/src/cloud/lib/i18n/enUS.ts +++ b/src/cloud/lib/i18n/enUS.ts @@ -377,7 +377,7 @@ const enTranslation: TranslationSource = { [lngKeys.ExpirationDate]: 'expiration date', [lngKeys.SeeFullHistory]: 'See full history', [lngKeys.SeeLimitedHistory]: 'See last {{days}} days', - [lngKeys.ThreadsTitle]: 'Threads', + [lngKeys.ThreadsTitle]: 'Comments', [lngKeys.ThreadPost]: 'Post', [lngKeys.ThreadFullDocLabel]: 'Full doc thread', [lngKeys.ThreadCreate]: 'Create a new thread', diff --git a/src/mobile/components/pages/DocViewPage.tsx b/src/mobile/components/pages/DocViewPage.tsx index b6ca2a7b42..135448ab71 100644 --- a/src/mobile/components/pages/DocViewPage.tsx +++ b/src/mobile/components/pages/DocViewPage.tsx @@ -3,14 +3,10 @@ import { SerializedDocWithSupplemental, SerializedDoc, } from '../../../cloud/interfaces/db/doc' -import { usePreferences } from '../../lib/preferences' import { SerializedUser } from '../../../cloud/interfaces/db/user' import useRealtime from '../../../cloud/lib/editor/hooks/useRealtime' import { buildIconUrl } from '../../../cloud/api/files' import { getColorFromString } from '../../../cloud/lib/utils/string' -import { createAbsolutePositionFromRelativePosition } from 'yjs' -import useCommentManagerState from '../../../cloud/lib/hooks/useCommentManagerState' -import { HighlightRange } from '../../../cloud/lib/rehypeHighlight' import Spinner from '../../../design/components/atoms/Spinner' import AppLayout from '../layouts/AppLayout' import NavigationBarButton from '../atoms/NavigationBarButton' @@ -39,7 +35,6 @@ const ViewPage = ({ contributors, backLinks, }: ViewPageProps) => { - const { setPreferences } = usePreferences() const { openModal } = useModal() const initialRenderDone = useRef(false) const previewRef = useRef(null) @@ -72,53 +67,6 @@ const ViewPage = ({ } }) - const [commentState, commentActions] = useCommentManagerState(doc.id) - - const [viewComments, setViewComments] = useState([]) - const calculatePositions = useCallback(() => { - if (commentState.mode === 'list_loading' || realtime == null) { - return - } - - const comments: HighlightRange[] = [] - for (const thread of commentState.threads) { - if (thread.selection != null && thread.status.type !== 'outdated') { - const absoluteAnchor = createAbsolutePositionFromRelativePosition( - thread.selection.anchor, - realtime.doc - ) - const absoluteHead = createAbsolutePositionFromRelativePosition( - thread.selection.head, - realtime.doc - ) - - if ( - absoluteAnchor != null && - absoluteHead != null && - absoluteAnchor.index !== absoluteHead.index - ) { - if (thread.status.type === 'open') { - comments.push({ - id: thread.id, - start: absoluteAnchor.index, - end: absoluteHead.index, - active: - commentState.mode === 'thread' && - thread.id === commentState.thread.id, - }) - } - } else if (connState === 'synced') { - commentActions.threadOutdated(thread) - } - } - } - setViewComments(comments) - }, [commentState, realtime, commentActions, connState]) - - useEffect(() => { - calculatePositions() - }, [calculatePositions]) - const updateContent = useCallback(() => { if (realtime == null) { return @@ -133,31 +81,15 @@ const ViewPage = ({ useEffect(() => { if (realtime != null) { realtime.doc.on('update', () => { - calculatePositions() updateContent() }) return () => realtime.doc.off('update', () => { - calculatePositions updateContent() }) } return undefined - }, [realtime, calculatePositions, updateContent]) - - const commentClick = useCallback( - (ids: string[]) => { - if (commentState.mode !== 'list_loading') { - const idSet = new Set(ids) - setPreferences({ docContextMode: 'comment' }) - commentActions.setMode({ - mode: 'list', - filter: (thread) => idSet.has(thread.id), - }) - } - }, - [commentState, commentActions, setPreferences] - ) + }, [realtime, updateContent]) useEffect(() => { if (connState === 'synced' || connState === 'loaded') { @@ -210,8 +142,6 @@ const ViewPage = ({ onRender={onRender.current} className='scroller' scrollerRef={previewRef} - comments={viewComments} - commentClick={commentClick} /> ) : ( <> @@ -227,23 +157,6 @@ const ViewPage = ({ ) } -const StyledLoadingView = styled.div` - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - text-align: center; - & span { - width: 100%; - height: 38px; - position: relative; - } -` - -const StyledPlaceholderContent = styled.div` - color: ${({ theme }) => theme.colors.text.subtle}; -` - const Container = styled.div` margin: 0; padding: 0; @@ -270,17 +183,17 @@ const Container = styled.div` ${({ theme }) => theme.sizes.spaces.xl}px ); font-size: 15px; - ${rightSidePageLayout} + ${rightSidePageLayout}; margin: auto; padding: 0 ${({ theme }) => theme.sizes.spaces.xl}px; } .view__content { height: 100%; - width: 50%; + width: 100%; padding-top: ${({ theme }) => theme.sizes.spaces.sm}px; + margin: 0 auto; - width: 100%; & .inline-comment.active, .inline-comment.hv-active { @@ -289,4 +202,21 @@ const Container = styled.div` } ` +const StyledLoadingView = styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + text-align: center; + & span { + width: 100%; + height: 38px; + position: relative; + } +` + +const StyledPlaceholderContent = styled.div` + color: ${({ theme }) => theme.colors.text.subtle}; +` + export default ViewPage From 7b806a6c91de401d42daf31ac3029d743c533b47 Mon Sep 17 00:00:00 2001 From: Komediruzecki Date: Thu, 20 Jan 2022 22:39:22 +0100 Subject: [PATCH 2/7] Fix user icons --- src/cloud/components/Comments/CommentList.tsx | 2 + src/cloud/components/Comments/ThreadItem.tsx | 49 ++++++++++++------- src/cloud/components/UserIcon.tsx | 2 +- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/cloud/components/Comments/CommentList.tsx b/src/cloud/components/Comments/CommentList.tsx index de0ae68335..1156cf75cc 100644 --- a/src/cloud/components/Comments/CommentList.tsx +++ b/src/cloud/components/Comments/CommentList.tsx @@ -200,6 +200,7 @@ const CommentItemContainer = styled.div` right: 7px; padding: 4px; + gap: 4px; border-radius: ${({ theme }) => theme.borders.radius}px; background-color: #1e2024; @@ -207,6 +208,7 @@ const CommentItemContainer = styled.div` .comment__meta__actions__edit, .comment__meta__actions__remove { height: 20px; + margin: 3px; color: ${({ theme }) => theme.colors.text.subtle}; diff --git a/src/cloud/components/Comments/ThreadItem.tsx b/src/cloud/components/Comments/ThreadItem.tsx index 49c16b4fa4..4f0cffe4d5 100644 --- a/src/cloud/components/Comments/ThreadItem.tsx +++ b/src/cloud/components/Comments/ThreadItem.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import { isToday, format, formatDistanceToNow } from 'date-fns' import { Thread, Comment } from '../../interfaces/db/comments' import UserIcon from '../UserIcon' @@ -85,6 +85,20 @@ function ThreadItem({ [onDelete, reloadComments] ) + const threadCommentedUser = useMemo(() => { + if (threadComments == null || threadComments.length == 0) { + return + } + + for (const user of users) { + if (threadComments[0].user.id == user.id) { + return user + } + } + + return threadComments[0].user + }, [threadComments, users]) + return (
- {thread.contributors.map((user) => ( - - ))} +
{threadComments && threadComments.length > 0 && (
@@ -125,14 +140,14 @@ function ThreadItem({ {threadComments && threadComments.length > 1 && (
- {thread.contributors.map((user) => ( - - ))} +
onSelect(thread)} className={'thread__comment__line__replies__link'} diff --git a/src/cloud/components/UserIcon.tsx b/src/cloud/components/UserIcon.tsx index ebdcff5cc5..078a1c8baf 100644 --- a/src/cloud/components/UserIcon.tsx +++ b/src/cloud/components/UserIcon.tsx @@ -25,7 +25,7 @@ const UserIcon = ({ user, style, className }: UserIconProps) => { export default UserIcon export const StyledUserIcon = styled.div` - ${userIconStyle} + ${userIconStyle}; width: 30px; height: 30px; border: 2px solid currentColor; From 13219138323412eb819bb6a88bdf91c320ec79ec Mon Sep 17 00:00:00 2001 From: Komediruzecki Date: Thu, 20 Jan 2022 22:44:37 +0100 Subject: [PATCH 3/7] Fix back button margin --- src/cloud/components/Comments/CommentManager.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cloud/components/Comments/CommentManager.tsx b/src/cloud/components/Comments/CommentManager.tsx index ae1ee3c017..9ef50ebb5d 100644 --- a/src/cloud/components/Comments/CommentManager.tsx +++ b/src/cloud/components/Comments/CommentManager.tsx @@ -224,6 +224,10 @@ const Container = styled.div` margin-left: ${({ theme }) => theme.sizes.spaces.sm}px; + .thread__content__back_button { + margin-top: ${({ theme }) => theme.sizes.spaces.sm}px; + } + & .comment__list { & > div { margin-bottom: ${({ theme }) => theme.sizes.spaces.df}px; From add8fd7e16e143a6cf52c51403c83792d90e5e2d Mon Sep 17 00:00:00 2001 From: Komediruzecki Date: Fri, 21 Jan 2022 20:32:23 +0100 Subject: [PATCH 4/7] Modify behavior on new comment No comment list on new thread --- src/cloud/lib/hooks/useCommentManagerState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cloud/lib/hooks/useCommentManagerState.ts b/src/cloud/lib/hooks/useCommentManagerState.ts index d96d4c694b..0d8d63e468 100644 --- a/src/cloud/lib/hooks/useCommentManagerState.ts +++ b/src/cloud/lib/hooks/useCommentManagerState.ts @@ -54,7 +54,7 @@ function useCommentManagerState(docId: string): [State, Actions] { async (data) => { const thread = await threadActions.create({ doc: docId, ...data }) if (!(thread instanceof Error)) { - setMode({ mode: 'thread', thread }) + setMode({ mode: 'list' }) } return thread }, From 836d67a1b7ccf89c7f881aa0f20f55a735fe2465 Mon Sep 17 00:00:00 2001 From: Komediruzecki Date: Wed, 26 Jan 2022 22:17:20 +0100 Subject: [PATCH 5/7] Fix re-focus on comment Remove initial content reset feature (no need) --- .../components/Comments/CommentInput.tsx | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/src/cloud/components/Comments/CommentInput.tsx b/src/cloud/components/Comments/CommentInput.tsx index 320212e8f5..7e6f145d61 100644 --- a/src/cloud/components/Comments/CommentInput.tsx +++ b/src/cloud/components/Comments/CommentInput.tsx @@ -24,6 +24,7 @@ interface CommentInputProps { } const smallUserIconStyle = { width: '20px', height: '20px', lineHeight: '17px' } + export function CommentInput({ onSubmit, value = '', @@ -78,10 +79,6 @@ export function CommentInput({ inputRef.current.addEventListener('blur', closeSuggestions) if (value.length > 0) { inputRef.current.appendChild(toFragment(value)) - } else { - // todo: [komediruzecki-2022-01-8] see why this was done... - makes placeholder impossible with pseudo:class - // maybe we could leave this and add placeholder directly, but then we need to remove it on type start... - // resetInitialContent(inputRef.current) } if (autoFocus) { inputRef.current.focus() @@ -95,11 +92,12 @@ export function CommentInput({ setWorking(true) await onSubmit(fromNode(inputRef.current).trim()) if (inputRef.current != null) { - resetInitialContent(inputRef.current) - inputRef.current.focus() + inputRef.current.innerHTML = '' + setIsInputEmpty(true) } } finally { setWorking(false) + inputRef.current.focus() } } }, [onSubmit]) @@ -116,7 +114,6 @@ export function CommentInput({ const inputContent = inputRef.current !== null ? fromNode(inputRef.current).trim() : '' - // setIsInputEmpty(inputContent === '') if ( ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && @@ -224,6 +221,7 @@ export function CommentInput({ const InputContainer = styled.div` position: relative; width: 100%; + & .comment__input__editable { white-space: pre-wrap; resize: none; @@ -267,16 +265,6 @@ const InputContainer = styled.div` } ` -function resetInitialContent(element: Element) { - for (let i = 0; i < element.childNodes.length; i++) { - element.removeChild(element.childNodes[i]) - } - - const child = document.createElement('div') - child.appendChild(document.createElement('br')) - element.appendChild(child) -} - function getMentionInSelection() { const selection = getSelection() if (selection == null) return null From 79e82e145216c6df1c17016a42b1e0158372f111 Mon Sep 17 00:00:00 2001 From: Komediruzecki Date: Wed, 26 Jan 2022 23:30:13 +0100 Subject: [PATCH 6/7] Update how comment scroll works Remove post submit button --- .../components/Comments/CommentInput.tsx | 31 ++--------------- src/cloud/components/Comments/CommentList.tsx | 1 - .../components/Comments/CommentManager.tsx | 33 +++++++++++-------- 3 files changed, 22 insertions(+), 43 deletions(-) diff --git a/src/cloud/components/Comments/CommentInput.tsx b/src/cloud/components/Comments/CommentInput.tsx index 7e6f145d61..374ec11694 100644 --- a/src/cloud/components/Comments/CommentInput.tsx +++ b/src/cloud/components/Comments/CommentInput.tsx @@ -1,5 +1,4 @@ import React, { useState, useCallback, useRef, useMemo } from 'react' -import Button from '../../../design/components/atoms/Button' import styled from '../../../design/lib/styled' import { useEffectOnce } from 'react-use' import useSuggestions from '../../../design/lib/hooks/useSuggestions' @@ -11,9 +10,6 @@ import { toFragment, isMention, } from '../../lib/comments' -import { lngKeys } from '../../lib/i18n/types' -import { useI18n } from '../../lib/hooks/useI18n' -import Flexbox from '../../../design/components/atoms/Flexbox' interface CommentInputProps { onSubmit: (comment: string) => any @@ -33,9 +29,7 @@ export function CommentInput({ placeholder, }: CommentInputProps) { const [working, setWorking] = useState(false) - const [isInputEmpty, setIsInputEmpty] = useState(false) const inputRef = useRef(null) - const { translate } = useI18n() const onSuggestionSelect = useRef((item: SerializedUser, hint: string) => { if (inputRef.current == null) { return @@ -93,7 +87,6 @@ export function CommentInput({ await onSubmit(fromNode(inputRef.current).trim()) if (inputRef.current != null) { inputRef.current.innerHTML = '' - setIsInputEmpty(true) } } finally { setWorking(false) @@ -102,12 +95,6 @@ export function CommentInput({ } }, [onSubmit]) - const onKeyUp = useCallback(() => { - const inputContent = - inputRef.current !== null ? fromNode(inputRef.current).trim() : '' - setIsInputEmpty(inputContent === '') - }, []) - const onKeyDown: React.KeyboardEventHandler = useCallback( (ev) => { onKeyDownListener(ev) @@ -165,31 +152,18 @@ export function CommentInput({ } }, []) - const onCommentInput = useCallback(() => { - if (inputRef.current != null) { - setIsInputEmpty(fromNode(inputRef.current).trim() === '') - } - }, []) - return (
- - - {state.type === 'enabled' && state.suggestions.length > 0 && (
theme.colors.border.second}; - min-height: 60px; + min-height: 30px; background-color: ${({ theme }) => theme.colors.background.secondary}; color: ${({ theme }) => theme.colors.text.primary}; padding: 5px 10px; diff --git a/src/cloud/components/Comments/CommentList.tsx b/src/cloud/components/Comments/CommentList.tsx index 1156cf75cc..124bbec23c 100644 --- a/src/cloud/components/Comments/CommentList.tsx +++ b/src/cloud/components/Comments/CommentList.tsx @@ -61,7 +61,6 @@ interface CommentItemProps { const smallUserIconStyle = { width: '28px', height: '28px', lineHeight: '22px' } -// todo: [komediruzecki-2022-01-18] Num of replies not reflect upon removal of item! export function CommentItem({ comment, editable, diff --git a/src/cloud/components/Comments/CommentManager.tsx b/src/cloud/components/Comments/CommentManager.tsx index 9ef50ebb5d..b9c0d55707 100644 --- a/src/cloud/components/Comments/CommentManager.tsx +++ b/src/cloud/components/Comments/CommentManager.tsx @@ -78,14 +78,16 @@ function CommentManager({ ) case 'list': { return ( -
- setMode({ mode: 'thread', thread })} - onDelete={deleteThread} - users={usersOrEmpty} - updateComment={updateComment} - /> + <> +
+ setMode({ mode: 'thread', thread })} + onDelete={deleteThread} + users={usersOrEmpty} + updateComment={updateComment} + /> +
-
+ ) } case 'thread': { @@ -137,7 +139,6 @@ function CommentManager({ case 'new_thread': { return (
- {/*
{state.data.context}
*/} { @@ -175,6 +176,7 @@ function CommentManager({ const Container = styled.div` margin: auto; height: 100vh; + overflow: hidden; width: 480px; display: flex; flex-direction: column; @@ -184,7 +186,7 @@ const Container = styled.div` color: ${({ theme }) => theme.colors.text.primary}; font-size: ${({ theme }) => theme.sizes.fonts.md}px; position: relative; - scrollbar-width: thin; + &::-webkit-scrollbar { width: 6px; } @@ -282,9 +284,12 @@ const Container = styled.div` display: flex; flex-direction: column; flex: 1; - .thread__list__container__create__thread { - margin-top: auto; - } + height: 100%; + overflow-y: auto; + } + + .thread__list__container__create__thread { + margin-top: ${({ theme }) => theme.sizes.spaces.df}px; } ` From d7342756dcae2ddad98a2e8ecae945518fe3ab52 Mon Sep 17 00:00:00 2001 From: Komediruzecki Date: Sat, 29 Jan 2022 12:21:16 +0100 Subject: [PATCH 7/7] Add send button Fix position relative when scrolling comments --- .../components/Comments/CommentInput.tsx | 121 ++++++++++++------ src/cloud/components/Comments/CommentList.tsx | 1 + 2 files changed, 83 insertions(+), 39 deletions(-) diff --git a/src/cloud/components/Comments/CommentInput.tsx b/src/cloud/components/Comments/CommentInput.tsx index 374ec11694..13d06f1a91 100644 --- a/src/cloud/components/Comments/CommentInput.tsx +++ b/src/cloud/components/Comments/CommentInput.tsx @@ -10,6 +10,8 @@ import { toFragment, isMention, } from '../../lib/comments' +import Button from '../../../design/components/atoms/Button' +import { mdiSendOutline } from '@mdi/js' interface CommentInputProps { onSubmit: (comment: string) => any @@ -21,7 +23,7 @@ interface CommentInputProps { const smallUserIconStyle = { width: '20px', height: '20px', lineHeight: '17px' } -export function CommentInput({ +function CommentInput({ onSubmit, value = '', autoFocus = false, @@ -30,6 +32,7 @@ export function CommentInput({ }: CommentInputProps) { const [working, setWorking] = useState(false) const inputRef = useRef(null) + const [isInputEmpty, setIsInputEmpty] = useState(true) const onSuggestionSelect = useRef((item: SerializedUser, hint: string) => { if (inputRef.current == null) { return @@ -80,18 +83,21 @@ export function CommentInput({ } }) - const submit = useCallback(async () => { - if (inputRef.current != null) { - try { - setWorking(true) - await onSubmit(fromNode(inputRef.current).trim()) - if (inputRef.current != null) { - inputRef.current.innerHTML = '' - } - } finally { - setWorking(false) - inputRef.current.focus() - } + const onPostCommentAction = useCallback(async () => { + if (inputRef.current == null) { + return + } + const inputContent = fromNode(inputRef.current).trim() + if (inputContent === '') { + return + } + try { + setWorking(true) + await onSubmit(fromNode(inputRef.current).trim()) + inputRef.current.innerHTML = '' + } finally { + setWorking(false) + inputRef.current.focus() } }, [onSubmit]) @@ -99,17 +105,13 @@ export function CommentInput({ (ev) => { onKeyDownListener(ev) - const inputContent = - inputRef.current !== null ? fromNode(inputRef.current).trim() : '' - if ( - ev.key === 'Enter' && - (ev.ctrlKey || ev.metaKey) && - inputContent !== '' - ) { - ev.preventDefault() - ev.stopPropagation() - submit() - return + if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) { + if (inputRef.current != null) { + ev.preventDefault() + ev.stopPropagation() + onPostCommentAction() + return + } } if (ev.key === 'Enter' && ev.shiftKey) { @@ -125,9 +127,21 @@ export function CommentInput({ } } }, - [submit, onKeyDownListener] + [onKeyDownListener, onPostCommentAction] ) + const onKeyUp = useCallback(() => { + const inputContent = + inputRef.current !== null ? fromNode(inputRef.current).trim() : '' + setIsInputEmpty(inputContent === '') + }, []) + + const onCommentInput = useCallback(() => { + if (inputRef.current != null) { + setIsInputEmpty(fromNode(inputRef.current).trim() === '') + } + }, []) + const selectSuggestion: React.MouseEventHandler = useCallback( (ev) => { ev.stopPropagation() @@ -154,16 +168,30 @@ export function CommentInput({ return ( -
+
+
+
+
+
{state.type === 'enabled' && state.suggestions.length > 0 && (
theme.sizes.spaces.l}px; + + .comment__input__send_button { + position: absolute; + right: 30px; + bottom: 6px; + } + } + + & .comment__input__input__editable_container { margin: auto; width: 90%; - white-space: pre-wrap; - resize: none; border: 1px solid ${({ theme }) => theme.colors.border.second}; min-height: 30px; background-color: ${({ theme }) => theme.colors.background.secondary}; color: ${({ theme }) => theme.colors.text.primary}; padding: 5px 10px; - margin-bottom: ${({ theme }) => theme.sizes.spaces.df}px; border-radius: ${({ theme }) => theme.borders.radius}px; + } + + & .comment__input__editable { + white-space: pre-wrap; + resize: none; + margin-bottom: ${({ theme }) => theme.sizes.spaces.md}px; &:empty:before { content: attr(data-placeholder); diff --git a/src/cloud/components/Comments/CommentList.tsx b/src/cloud/components/Comments/CommentList.tsx index 124bbec23c..ab44100f04 100644 --- a/src/cloud/components/Comments/CommentList.tsx +++ b/src/cloud/components/Comments/CommentList.tsx @@ -155,6 +155,7 @@ const CommentItemContainer = styled.div` display: flex; align-items: center; margin-bottom: 4px; + position: relative; & svg { color: ${({ theme }) => theme.colors.icon.default};