diff --git a/src/components/Article/Comment.module.scss b/src/components/Article/Comment.module.scss index 7a48dec2..786fe41d 100644 --- a/src/components/Article/Comment.module.scss +++ b/src/components/Article/Comment.module.scss @@ -150,7 +150,6 @@ } .commentAuthor, -.commentDate, .commentRating { @include font-size(1.2rem); } @@ -161,11 +160,30 @@ margin-right: 12px; } -.commentDate { - color: rgb(0 0 0 / 30%); +.commentDates { flex: 1; - @include media-breakpoint-down(md) { - margin-left: 1rem; + display: flex; + gap: 1rem; + align-items: center; + justify-content: flex-start; + color: rgba(0, 0, 0, 0.3); + font-size: 1.2rem; + margin-bottom: 4px; + color: rgb(0 0 0 / 30%); + @include font-size(1.2rem); + + .date { + .icon { + line-height: 1; + width: 1rem; + display: inline-block; + opacity: 0.6; + margin-right: 0.5rem; + vertical-align: middle; + } + @include media-breakpoint-down(md) { + margin-left: 1rem; + } } } diff --git a/src/components/Article/Comment.tsx b/src/components/Article/Comment.tsx index 2c0b7045..c108895e 100644 --- a/src/components/Article/Comment.tsx +++ b/src/components/Article/Comment.tsx @@ -1,21 +1,17 @@ import styles from './Comment.module.scss' import { Icon } from '../_shared/Icon' import { AuthorCard } from '../Author/Card' -import { Show, createMemo, createSignal, For } from 'solid-js' +import { Show, createMemo, createSignal, For, lazy, Suspense } from 'solid-js' import { clsx } from 'clsx' import type { Author, Reaction } from '../../graphql/types.gen' import { t } from '../../utils/intl' -import { createReaction, deleteReaction } from '../../stores/zine/reactions' +import { createReaction, deleteReaction, updateReaction } from '../../stores/zine/reactions' import MD from './MD' import { formatDate } from '../../utils' -import { SharePopup } from './SharePopup' -import stylesHeader from '../Nav/Header.module.scss' import Userpic from '../Author/Userpic' import { useSession } from '../../context/session' import { ReactionKind } from '../../graphql/types.gen' -import CommentEditor from '../_shared/CommentEditor' -import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' -import { getDescription } from '../../utils/meta' +const CommentEditor = lazy(() => import('../_shared/CommentEditor')) type Props = { comment: Reaction @@ -27,7 +23,7 @@ type Props = { export const Comment = (props: Props) => { const [isReplyVisible, setIsReplyVisible] = createSignal(false) const [loading, setLoading] = createSignal(false) - const [submitted, setSubmitted] = createSignal(false) + const [editMode, setEditMode] = createSignal(false) const { session } = useSession() const canEdit = createMemo(() => props.comment.createdBy?.slug === session()?.user?.slug) @@ -61,16 +57,33 @@ export const Comment = (props: Props) => { } ) setIsReplyVisible(false) - setSubmitted(true) setLoading(false) } catch (error) { console.error('[handleCreate reaction]:', error) } } - const formattedDate = createMemo(() => - formatDate(new Date(comment()?.createdAt), { hour: 'numeric', minute: 'numeric' }) - ) + const formattedDate = (date) => + createMemo(() => formatDate(new Date(date), { hour: 'numeric', minute: 'numeric' })) + + const toggleEditMode = () => { + setEditMode((oldEditMode) => !oldEditMode) + } + + const handleUpdate = async (value) => { + setLoading(true) + try { + await updateReaction(props.comment.id, { + kind: ReactionKind.Comment, + body: value, + shout: props.comment.shout.id + }) + setEditMode(false) + setLoading(false) + } catch (error) { + console.error('[handleCreate reaction]:', error) + } + } return (
  • @@ -102,7 +115,15 @@ export const Comment = (props: Props) => {
    {t('Author')}
    -
    {formattedDate()}
    +
    +
    {formattedDate(comment()?.createdAt)}
    + +
    + + {t('Edited')} {formattedDate(comment()?.updatedAt)} +
    +
    +
    {
    -
    - +
    + }> + Loading...

    }> + handleUpdate(value)} /> +
    +
    @@ -136,14 +157,13 @@ export const Comment = (props: Props) => { - {/*FIXME implement edit comment modal*/} - {/* showModal('editComment')}*/} - {/*>*/} - {/* */} - {/* {t('Edit')}*/} - {/**/} +
    - - handleCreate(value)} - /> - + {t('Loading')}

    }> + handleCreate(value)} /> +
    diff --git a/src/components/Article/CommentsTree.tsx b/src/components/Article/CommentsTree.tsx index 67eb9f77..5092b0b6 100644 --- a/src/components/Article/CommentsTree.tsx +++ b/src/components/Article/CommentsTree.tsx @@ -1,4 +1,4 @@ -import { For, Show, createMemo, createSignal, onMount } from 'solid-js' +import { Show, createMemo, createSignal, onMount, For } from 'solid-js' import Comment from './Comment' import { t } from '../../utils/intl' import styles from '../../styles/Article.module.scss' @@ -11,6 +11,7 @@ import { Author, ReactionKind } from '../../graphql/types.gen' import { useSession } from '../../context/session' import CommentEditor from '../_shared/CommentEditor' import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' +import Button from '../_shared/Button' const ARTICLE_COMMENTS_PAGE_SIZE = 50 const MAX_COMMENT_LEVEL = 6 @@ -27,9 +28,11 @@ export const CommentsTree = (props: Props) => { const [isCommentsLoading, setIsCommentsLoading] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const { sortedReactions, loadReactionsBy } = useReactionsStore() + const reactions = createMemo(() => sortedReactions().sort(commentsOrder() === 'rating' ? byStat('rating') : byCreated) ) + const { session } = useSession() const loadMore = async () => { try { @@ -85,26 +88,22 @@ export const CommentsTree = (props: Props) => { @@ -128,7 +127,7 @@ export const CommentsTree = (props: Props) => { handleSubmitComment(value)} /> diff --git a/src/components/_shared/Button/Button.module.scss b/src/components/_shared/Button/Button.module.scss index f1e08220..50e510d7 100644 --- a/src/components/_shared/Button/Button.module.scss +++ b/src/components/_shared/Button/Button.module.scss @@ -31,6 +31,19 @@ } } + &.inline { + font-weight: 700; + font-size: 16px; + line-height: 21px; + color: #696969; + + &.hover, + &.active { + text-decoration: underline; + color: #141414; + } + } + &:disabled, &:disabled:hover { cursor: default; diff --git a/src/components/_shared/Button/Button.tsx b/src/components/_shared/Button/Button.tsx index 109f3783..5a3fb3fa 100644 --- a/src/components/_shared/Button/Button.tsx +++ b/src/components/_shared/Button/Button.tsx @@ -5,7 +5,7 @@ import styles from './Button.module.scss' type Props = { value: string | JSX.Element size?: 'S' | 'M' | 'L' - variant?: 'primary' | 'secondary' + variant?: 'primary' | 'secondary' | 'inline' type?: 'submit' | 'button' loading?: boolean disabled?: boolean diff --git a/src/components/_shared/CommentEditor/CommentEditor.tsx b/src/components/_shared/CommentEditor/CommentEditor.tsx index de857fc1..21175293 100644 --- a/src/components/_shared/CommentEditor/CommentEditor.tsx +++ b/src/components/_shared/CommentEditor/CommentEditor.tsx @@ -8,7 +8,7 @@ import { t } from '../../../utils/intl' import { schema } from './schema' import { EditorState } from 'prosemirror-state' import { EditorView } from 'prosemirror-view' -import { DOMSerializer } from 'prosemirror-model' +import { DOMParser as ProseDOMParser, DOMSerializer } from 'prosemirror-model' import { renderGrouped } from 'prosemirror-menu' import { buildMenuItems } from './menu' import { keymap } from 'prosemirror-keymap' @@ -20,9 +20,10 @@ import { useSession } from '../../../context/session' import { showModal } from '../../../stores/ui' type Props = { - initialValue: string + placeholder?: string onSubmit: (value: string) => void clear?: boolean + initialContent?: string } const htmlContainer = typeof document === 'undefined' ? null : document.createElement('div') @@ -37,14 +38,19 @@ const CommentEditor = (props: Props) => { const editorElRef: { current: HTMLDivElement } = { current: null } const menuElRef: { current: HTMLDivElement } = { current: null } const editorViewRef: { current: EditorView } = { current: null } + + const domNew = new DOMParser().parseFromString(`
    ${props.initialContent}
    `, 'text/xml') + const doc = ProseDOMParser.fromSchema(schema).parse(domNew) + const initEditor = () => { editorViewRef.current = new EditorView(editorElRef.current, { state: EditorState.create({ schema, + doc: props.initialContent ? doc : null, plugins: [ history(), customKeymap(), - placeholder(props.initialValue), + placeholder(props.placeholder), keymap({ 'Mod-z': undo, 'Mod-y': redo }), keymap(baseKeymap) ] diff --git a/src/graphql/mutation/reaction-destroy.ts b/src/graphql/mutation/reaction-destroy.ts index 8be2fa78..abc1bf05 100644 --- a/src/graphql/mutation/reaction-destroy.ts +++ b/src/graphql/mutation/reaction-destroy.ts @@ -1,8 +1,8 @@ import { gql } from '@urql/core' export default gql` - mutation DeleteReactionMutation($reaction: Int!) { - deleteReaction(reaction: $reaction) { + mutation DeleteReactionMutation($id: Int!) { + deleteReaction(id: $id) { error reaction { id diff --git a/src/graphql/mutation/reaction-update.ts b/src/graphql/mutation/reaction-update.ts index 325e96e8..c9650ae5 100644 --- a/src/graphql/mutation/reaction-update.ts +++ b/src/graphql/mutation/reaction-update.ts @@ -1,32 +1,13 @@ import { gql } from '@urql/core' export default gql` - mutation UpdateReactionMutation($reaction: ReactionInput!) { - updateReaction(reaction: $reaction) { + mutation UpdateReactionMutation($id: Int!, $reaction: ReactionInput!) { + updateReaction(id: $id, reaction: $reaction) { error reaction { - id - createdBy { - slug - name - userpic - } body - kind - range - createdAt updatedAt - shout - replyTo { - id - createdBy { - slug - userpic - name - } - body - kind - } + replyTo } } } diff --git a/src/graphql/types.gen.ts b/src/graphql/types.gen.ts index 620d5f56..94cb09eb 100644 --- a/src/graphql/types.gen.ts +++ b/src/graphql/types.gen.ts @@ -537,11 +537,13 @@ export enum ReactionKind { Disagree = 'DISAGREE', Dislike = 'DISLIKE', Disproof = 'DISPROOF', + Footnote = 'FOOTNOTE', Like = 'LIKE', Proof = 'PROOF', Propose = 'PROPOSE', Quote = 'QUOTE', - Reject = 'REJECT' + Reject = 'REJECT', + Remark = 'REMARK' } export enum ReactionStatus { @@ -656,12 +658,9 @@ export type Stat = { } export type Subscription = { - collabUpdate?: Maybe newMessage?: Maybe -} - -export type SubscriptionCollabUpdateArgs = { - collab: Scalars['Int'] + newReaction?: Maybe + newShout?: Maybe } export type Token = { diff --git a/src/locales/ru.json b/src/locales/ru.json index cd78234d..b086056e 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -17,6 +17,7 @@ "By name": "По имени", "By popularity": "По популярности", "By rating": "По популярности", + "By time": "По времени", "By relevance": "По релевантности", "By shouts": "По публикациям", "By signing up you agree with our": "Регистрируясь, вы соглашаетесь с", @@ -223,6 +224,8 @@ "Add comment": "Комментировать", "My subscriptions": "Подписки", "Nothing here yet": "Здесь пока ничего нет", + "Edited": "Отредактирован", + "Nothing here yet": "Здесь пока ничего нет", "Invite experts": "Пригласить экспертов", "Subscribe to comments": "Подписаться на комментарии", "Add to bookmarks": "Добавить в закладки", diff --git a/src/stores/zine/reactions.ts b/src/stores/zine/reactions.ts index 6f50d75c..be479ce3 100644 --- a/src/stores/zine/reactions.ts +++ b/src/stores/zine/reactions.ts @@ -1,6 +1,6 @@ import type { Reaction, ReactionInput } from '../../graphql/types.gen' import { apiClient } from '../../utils/apiClient' -import { createSignal } from 'solid-js' +import { createEffect, createSignal } from 'solid-js' // TODO: import { roomConnect } from '../../utils/p2p' export const REACTIONS_AMOUNT_PER_PAGE = 100 @@ -34,15 +34,21 @@ export const createReaction = async ( setSortedReactions((prev) => [...prev, reaction]) } -export const deleteReaction = async (reactionId: number) => { - const reaction = await apiClient.destroyReaction(reactionId) +export const deleteReaction = async (id: number) => { + const reaction = await apiClient.destroyReaction(id) console.debug('[deleteReaction]:', reaction.reaction.id) setSortedReactions(sortedReactions().filter((item) => item.id !== reaction.reaction.id)) } -export const updateReaction = async (reaction: Reaction) => { - const { reaction: r } = await apiClient.updateReaction({ reaction }) - return r +export const updateReaction = async (id: number, input: ReactionInput) => { + const reaction = await apiClient.updateReaction(id, input) + const editedReactionIndex = sortedReactions().findIndex((r) => r.id === id) + const newSortedReactions = [...sortedReactions()] + newSortedReactions[editedReactionIndex] = { + ...newSortedReactions[editedReactionIndex], + ...reaction + } + setSortedReactions(newSortedReactions) } export const useReactionsStore = () => { diff --git a/src/utils/apiClient.ts b/src/utils/apiClient.ts index 39c405fc..8d805bc4 100644 --- a/src/utils/apiClient.ts +++ b/src/utils/apiClient.ts @@ -237,14 +237,16 @@ export const apiClient = { return response.data.createReaction.reaction }, destroyReaction: async (id: number) => { - const response = await privateGraphQLClient.mutation(reactionDestroy, { reaction: id }).toPromise() + const response = await privateGraphQLClient.mutation(reactionDestroy, { id: id }).toPromise() console.debug('[destroyReaction]:', response) return response.data.deleteReaction }, - updateReaction: async (reaction) => { - const response = await privateGraphQLClient.mutation(reactionUpdate, reaction).toPromise() - - return response.data.createReaction + updateReaction: async (id: number, input: ReactionInput) => { + const response = await privateGraphQLClient + .mutation(reactionUpdate, { id: id, reaction: input }) + .toPromise() + console.debug('[updateReaction]:', response) + return response.data.updateReaction.reaction }, getAuthorsBy: async (options: QueryLoadAuthorsByArgs) => { const resp = await publicGraphQLClient.query(authorsLoadBy, options).toPromise()