Merge remote-tracking branch 'gitlab/dev' into feed-article-popup

This commit is contained in:
bniwredyc 2023-02-07 14:31:18 +01:00
commit a84dac9577
12 changed files with 143 additions and 100 deletions

View File

@ -150,7 +150,6 @@
} }
.commentAuthor, .commentAuthor,
.commentDate,
.commentRating { .commentRating {
@include font-size(1.2rem); @include font-size(1.2rem);
} }
@ -161,12 +160,31 @@
margin-right: 12px; margin-right: 12px;
} }
.commentDate { .commentDates {
color: rgb(0 0 0 / 30%);
flex: 1; flex: 1;
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) { @include media-breakpoint-down(md) {
margin-left: 1rem; margin-left: 1rem;
} }
}
} }
.commentDetails { .commentDetails {

View File

@ -1,21 +1,17 @@
import styles from './Comment.module.scss' import styles from './Comment.module.scss'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { AuthorCard } from '../Author/Card' 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 { clsx } from 'clsx'
import type { Author, Reaction } from '../../graphql/types.gen' import type { Author, Reaction } from '../../graphql/types.gen'
import { t } from '../../utils/intl' 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 MD from './MD'
import { formatDate } from '../../utils' import { formatDate } from '../../utils'
import { SharePopup } from './SharePopup'
import stylesHeader from '../Nav/Header.module.scss'
import Userpic from '../Author/Userpic' import Userpic from '../Author/Userpic'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { ReactionKind } from '../../graphql/types.gen' import { ReactionKind } from '../../graphql/types.gen'
import CommentEditor from '../_shared/CommentEditor' const CommentEditor = lazy(() => import('../_shared/CommentEditor'))
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import { getDescription } from '../../utils/meta'
type Props = { type Props = {
comment: Reaction comment: Reaction
@ -27,7 +23,7 @@ type Props = {
export const Comment = (props: Props) => { export const Comment = (props: Props) => {
const [isReplyVisible, setIsReplyVisible] = createSignal(false) const [isReplyVisible, setIsReplyVisible] = createSignal(false)
const [loading, setLoading] = createSignal<boolean>(false) const [loading, setLoading] = createSignal<boolean>(false)
const [submitted, setSubmitted] = createSignal<boolean>(false) const [editMode, setEditMode] = createSignal<boolean>(false)
const { session } = useSession() const { session } = useSession()
const canEdit = createMemo(() => props.comment.createdBy?.slug === session()?.user?.slug) const canEdit = createMemo(() => props.comment.createdBy?.slug === session()?.user?.slug)
@ -61,16 +57,33 @@ export const Comment = (props: Props) => {
} }
) )
setIsReplyVisible(false) setIsReplyVisible(false)
setSubmitted(true)
setLoading(false) setLoading(false)
} catch (error) { } catch (error) {
console.error('[handleCreate reaction]:', error) console.error('[handleCreate reaction]:', error)
} }
} }
const formattedDate = createMemo(() => const formattedDate = (date) =>
formatDate(new Date(comment()?.createdAt), { hour: 'numeric', minute: 'numeric' }) 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 ( return (
<li class={styles.comment}> <li class={styles.comment}>
@ -102,7 +115,15 @@ export const Comment = (props: Props) => {
<div class={styles.articleAuthor}>{t('Author')}</div> <div class={styles.articleAuthor}>{t('Author')}</div>
</Show> </Show>
<div class={styles.commentDate}>{formattedDate()}</div> <div class={styles.commentDates}>
<div class={styles.date}>{formattedDate(comment()?.createdAt)}</div>
<Show when={comment()?.updatedAt}>
<div class={styles.date}>
<Icon name="edit" class={styles.icon} />
{t('Edited')} {formattedDate(comment()?.updatedAt)}
</div>
</Show>
</div>
<div <div
class={styles.commentRating} class={styles.commentRating}
classList={{ classList={{
@ -116,12 +137,12 @@ export const Comment = (props: Props) => {
</div> </div>
</div> </div>
</Show> </Show>
<div <div class={styles.commentBody} id={'comment-' + (comment().id || '')}>
class={styles.commentBody} <Show when={editMode()} fallback={<MD body={body()} />}>
contenteditable={canEdit()} <Suspense fallback={<p>Loading...</p>}>
id={'comment-' + (comment().id || '')} <CommentEditor initialContent={body()} onSubmit={(value) => handleUpdate(value)} />
> </Suspense>
<MD body={body()} /> </Show>
</div> </div>
<Show when={!props.compact}> <Show when={!props.compact}>
@ -136,14 +157,13 @@ export const Comment = (props: Props) => {
</button> </button>
<Show when={canEdit()}> <Show when={canEdit()}>
{/*FIXME implement edit comment modal*/} <button
{/*<button*/} class={clsx(styles.commentControl, styles.commentControlEdit)}
{/* class={clsx(styles.commentControl, styles.commentControlEdit)}*/} onClick={toggleEditMode}
{/* onClick={() => showModal('editComment')}*/} >
{/*>*/} <Icon name="edit" class={styles.icon} />
{/* <Icon name="edit" class={styles.icon} />*/} {t('Edit')}
{/* {t('Edit')}*/} </button>
{/*</button>*/}
<button <button
class={clsx(styles.commentControl, styles.commentControlDelete)} class={clsx(styles.commentControl, styles.commentControlDelete)}
onClick={() => remove()} onClick={() => remove()}
@ -174,13 +194,9 @@ export const Comment = (props: Props) => {
</div> </div>
<Show when={isReplyVisible()}> <Show when={isReplyVisible()}>
<ShowOnlyOnClient> <Suspense fallback={<p>{t('Loading')}</p>}>
<CommentEditor <CommentEditor placeholder={''} onSubmit={(value) => handleCreate(value)} />
initialValue={''} </Suspense>
clear={submitted()}
onSubmit={(value) => handleCreate(value)}
/>
</ShowOnlyOnClient>
</Show> </Show>
</Show> </Show>
</div> </div>

View File

@ -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 Comment from './Comment'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import styles from '../../styles/Article.module.scss' import styles from '../../styles/Article.module.scss'
@ -11,6 +11,7 @@ import { Author, ReactionKind } from '../../graphql/types.gen'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import CommentEditor from '../_shared/CommentEditor' import CommentEditor from '../_shared/CommentEditor'
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import Button from '../_shared/Button'
const ARTICLE_COMMENTS_PAGE_SIZE = 50 const ARTICLE_COMMENTS_PAGE_SIZE = 50
const MAX_COMMENT_LEVEL = 6 const MAX_COMMENT_LEVEL = 6
@ -27,9 +28,11 @@ export const CommentsTree = (props: Props) => {
const [isCommentsLoading, setIsCommentsLoading] = createSignal(false) const [isCommentsLoading, setIsCommentsLoading] = createSignal(false)
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const { sortedReactions, loadReactionsBy } = useReactionsStore() const { sortedReactions, loadReactionsBy } = useReactionsStore()
const reactions = createMemo<Reaction[]>(() => const reactions = createMemo<Reaction[]>(() =>
sortedReactions().sort(commentsOrder() === 'rating' ? byStat('rating') : byCreated) sortedReactions().sort(commentsOrder() === 'rating' ? byStat('rating') : byCreated)
) )
const { session } = useSession() const { session } = useSession()
const loadMore = async () => { const loadMore = async () => {
try { try {
@ -85,26 +88,22 @@ export const CommentsTree = (props: Props) => {
<ul class={clsx(styles.commentsViewSwitcher, 'view-switcher')}> <ul class={clsx(styles.commentsViewSwitcher, 'view-switcher')}>
<li classList={{ selected: commentsOrder() === 'createdAt' || !commentsOrder() }}> <li classList={{ selected: commentsOrder() === 'createdAt' || !commentsOrder() }}>
<a <Button
href="#" variant="inline"
onClick={(ev) => { value={t('By time')}
ev.preventDefault() onClick={() => {
setCommentsOrder('createdAt') setCommentsOrder('createdAt')
}} }}
> />
{t('By time')}
</a>
</li> </li>
<li classList={{ selected: commentsOrder() === 'rating' }}> <li classList={{ selected: commentsOrder() === 'rating' }}>
<a <Button
href="#" variant="inline"
onClick={(ev) => { value={t('By rating')}
ev.preventDefault() onClick={() => {
setCommentsOrder('rating') setCommentsOrder('rating')
}} }}
> />
{t('By rating')}
</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -128,7 +127,7 @@ export const CommentsTree = (props: Props) => {
</Show> </Show>
<ShowOnlyOnClient> <ShowOnlyOnClient>
<CommentEditor <CommentEditor
initialValue={t('Write a comment...')} placeholder={t('Write a comment...')}
clear={submitted()} clear={submitted()}
onSubmit={(value) => handleSubmitComment(value)} onSubmit={(value) => handleSubmitComment(value)}
/> />

View File

@ -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,
&:disabled:hover { &:disabled:hover {
cursor: default; cursor: default;

View File

@ -5,7 +5,7 @@ import styles from './Button.module.scss'
type Props = { type Props = {
value: string | JSX.Element value: string | JSX.Element
size?: 'S' | 'M' | 'L' size?: 'S' | 'M' | 'L'
variant?: 'primary' | 'secondary' variant?: 'primary' | 'secondary' | 'inline'
type?: 'submit' | 'button' type?: 'submit' | 'button'
loading?: boolean loading?: boolean
disabled?: boolean disabled?: boolean

View File

@ -8,7 +8,7 @@ import { t } from '../../../utils/intl'
import { schema } from './schema' import { schema } from './schema'
import { EditorState } from 'prosemirror-state' import { EditorState } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view' import { EditorView } from 'prosemirror-view'
import { DOMSerializer } from 'prosemirror-model' import { DOMParser as ProseDOMParser, DOMSerializer } from 'prosemirror-model'
import { renderGrouped } from 'prosemirror-menu' import { renderGrouped } from 'prosemirror-menu'
import { buildMenuItems } from './menu' import { buildMenuItems } from './menu'
import { keymap } from 'prosemirror-keymap' import { keymap } from 'prosemirror-keymap'
@ -20,9 +20,10 @@ import { useSession } from '../../../context/session'
import { showModal } from '../../../stores/ui' import { showModal } from '../../../stores/ui'
type Props = { type Props = {
initialValue: string placeholder?: string
onSubmit: (value: string) => void onSubmit: (value: string) => void
clear?: boolean clear?: boolean
initialContent?: string
} }
const htmlContainer = typeof document === 'undefined' ? null : document.createElement('div') const htmlContainer = typeof document === 'undefined' ? null : document.createElement('div')
@ -37,14 +38,19 @@ const CommentEditor = (props: Props) => {
const editorElRef: { current: HTMLDivElement } = { current: null } const editorElRef: { current: HTMLDivElement } = { current: null }
const menuElRef: { current: HTMLDivElement } = { current: null } const menuElRef: { current: HTMLDivElement } = { current: null }
const editorViewRef: { current: EditorView } = { current: null } const editorViewRef: { current: EditorView } = { current: null }
const domNew = new DOMParser().parseFromString(`<div>${props.initialContent}</div>`, 'text/xml')
const doc = ProseDOMParser.fromSchema(schema).parse(domNew)
const initEditor = () => { const initEditor = () => {
editorViewRef.current = new EditorView(editorElRef.current, { editorViewRef.current = new EditorView(editorElRef.current, {
state: EditorState.create({ state: EditorState.create({
schema, schema,
doc: props.initialContent ? doc : null,
plugins: [ plugins: [
history(), history(),
customKeymap(), customKeymap(),
placeholder(props.initialValue), placeholder(props.placeholder),
keymap({ 'Mod-z': undo, 'Mod-y': redo }), keymap({ 'Mod-z': undo, 'Mod-y': redo }),
keymap(baseKeymap) keymap(baseKeymap)
] ]

View File

@ -1,8 +1,8 @@
import { gql } from '@urql/core' import { gql } from '@urql/core'
export default gql` export default gql`
mutation DeleteReactionMutation($reaction: Int!) { mutation DeleteReactionMutation($id: Int!) {
deleteReaction(reaction: $reaction) { deleteReaction(id: $id) {
error error
reaction { reaction {
id id

View File

@ -1,32 +1,13 @@
import { gql } from '@urql/core' import { gql } from '@urql/core'
export default gql` export default gql`
mutation UpdateReactionMutation($reaction: ReactionInput!) { mutation UpdateReactionMutation($id: Int!, $reaction: ReactionInput!) {
updateReaction(reaction: $reaction) { updateReaction(id: $id, reaction: $reaction) {
error error
reaction { reaction {
id
createdBy {
slug
name
userpic
}
body body
kind
range
createdAt
updatedAt updatedAt
shout replyTo
replyTo {
id
createdBy {
slug
userpic
name
}
body
kind
}
} }
} }
} }

View File

@ -537,11 +537,13 @@ export enum ReactionKind {
Disagree = 'DISAGREE', Disagree = 'DISAGREE',
Dislike = 'DISLIKE', Dislike = 'DISLIKE',
Disproof = 'DISPROOF', Disproof = 'DISPROOF',
Footnote = 'FOOTNOTE',
Like = 'LIKE', Like = 'LIKE',
Proof = 'PROOF', Proof = 'PROOF',
Propose = 'PROPOSE', Propose = 'PROPOSE',
Quote = 'QUOTE', Quote = 'QUOTE',
Reject = 'REJECT' Reject = 'REJECT',
Remark = 'REMARK'
} }
export enum ReactionStatus { export enum ReactionStatus {
@ -656,12 +658,9 @@ export type Stat = {
} }
export type Subscription = { export type Subscription = {
collabUpdate?: Maybe<Reaction>
newMessage?: Maybe<Message> newMessage?: Maybe<Message>
} newReaction?: Maybe<Reaction>
newShout?: Maybe<Shout>
export type SubscriptionCollabUpdateArgs = {
collab: Scalars['Int']
} }
export type Token = { export type Token = {

View File

@ -17,6 +17,7 @@
"By name": "По имени", "By name": "По имени",
"By popularity": "По популярности", "By popularity": "По популярности",
"By rating": "По популярности", "By rating": "По популярности",
"By time": "По времени",
"By relevance": "По релевантности", "By relevance": "По релевантности",
"By shouts": "По публикациям", "By shouts": "По публикациям",
"By signing up you agree with our": "Регистрируясь, вы соглашаетесь с", "By signing up you agree with our": "Регистрируясь, вы соглашаетесь с",
@ -223,6 +224,8 @@
"Add comment": "Комментировать", "Add comment": "Комментировать",
"My subscriptions": "Подписки", "My subscriptions": "Подписки",
"Nothing here yet": "Здесь пока ничего нет", "Nothing here yet": "Здесь пока ничего нет",
"Edited": "Отредактирован",
"Nothing here yet": "Здесь пока ничего нет",
"Invite experts": "Пригласить экспертов", "Invite experts": "Пригласить экспертов",
"Subscribe to comments": "Подписаться на комментарии", "Subscribe to comments": "Подписаться на комментарии",
"Add to bookmarks": "Добавить в закладки", "Add to bookmarks": "Добавить в закладки",

View File

@ -1,6 +1,6 @@
import type { Reaction, ReactionInput } from '../../graphql/types.gen' import type { Reaction, ReactionInput } from '../../graphql/types.gen'
import { apiClient } from '../../utils/apiClient' import { apiClient } from '../../utils/apiClient'
import { createSignal } from 'solid-js' import { createEffect, createSignal } from 'solid-js'
// TODO: import { roomConnect } from '../../utils/p2p' // TODO: import { roomConnect } from '../../utils/p2p'
export const REACTIONS_AMOUNT_PER_PAGE = 100 export const REACTIONS_AMOUNT_PER_PAGE = 100
@ -34,15 +34,21 @@ export const createReaction = async (
setSortedReactions((prev) => [...prev, reaction]) setSortedReactions((prev) => [...prev, reaction])
} }
export const deleteReaction = async (reactionId: number) => { export const deleteReaction = async (id: number) => {
const reaction = await apiClient.destroyReaction(reactionId) const reaction = await apiClient.destroyReaction(id)
console.debug('[deleteReaction]:', reaction.reaction.id) console.debug('[deleteReaction]:', reaction.reaction.id)
setSortedReactions(sortedReactions().filter((item) => item.id !== reaction.reaction.id)) setSortedReactions(sortedReactions().filter((item) => item.id !== reaction.reaction.id))
} }
export const updateReaction = async (reaction: Reaction) => { export const updateReaction = async (id: number, input: ReactionInput) => {
const { reaction: r } = await apiClient.updateReaction({ reaction }) const reaction = await apiClient.updateReaction(id, input)
return r const editedReactionIndex = sortedReactions().findIndex((r) => r.id === id)
const newSortedReactions = [...sortedReactions()]
newSortedReactions[editedReactionIndex] = {
...newSortedReactions[editedReactionIndex],
...reaction
}
setSortedReactions(newSortedReactions)
} }
export const useReactionsStore = () => { export const useReactionsStore = () => {

View File

@ -237,14 +237,16 @@ export const apiClient = {
return response.data.createReaction.reaction return response.data.createReaction.reaction
}, },
destroyReaction: async (id: number) => { 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) console.debug('[destroyReaction]:', response)
return response.data.deleteReaction return response.data.deleteReaction
}, },
updateReaction: async (reaction) => { updateReaction: async (id: number, input: ReactionInput) => {
const response = await privateGraphQLClient.mutation(reactionUpdate, reaction).toPromise() const response = await privateGraphQLClient
.mutation(reactionUpdate, { id: id, reaction: input })
return response.data.createReaction .toPromise()
console.debug('[updateReaction]:', response)
return response.data.updateReaction.reaction
}, },
getAuthorsBy: async (options: QueryLoadAuthorsByArgs) => { getAuthorsBy: async (options: QueryLoadAuthorsByArgs) => {
const resp = await publicGraphQLClient.query(authorsLoadBy, options).toPromise() const resp = await publicGraphQLClient.query(authorsLoadBy, options).toPromise()