This commit is contained in:
tonyrewin 2023-01-20 13:50:29 +03:00
commit 9722c031a1
13 changed files with 157 additions and 97 deletions

View File

@ -1,18 +1,40 @@
.comment { .comment {
background-color: #fff; background-color: #fff;
margin: 0 -2.4rem 1.5em; margin: 0 -2.4rem 0.5em;
padding: 0.8rem 2.4rem; padding: 0.8rem 2.4rem;
transition: background-color 0.3s; transition: background-color 0.3s;
position: relative;
&:hover { &:last-child {
background-color: #f6f6f6; margin-bottom: 0;
}
.commentControlReply, .comment {
.commentControlShare, &:before,
.commentControlDelete, &:after {
.commentControlEdit, content: '';
.commentControlComplain { left: 0;
opacity: 1; position: absolute;
}
&:before {
border-bottom: 2px solid #ccc;
border-left: 2px solid #ccc;
border-radius: 0 0 0 1.2rem;
top: -1rem;
height: 2.4rem;
width: 1.2rem;
}
&:after {
background: #ccc;
height: 100%;
top: 0;
width: 2px;
}
&:last-child:after {
display: none;
} }
} }
@ -32,24 +54,16 @@
} }
} }
.commentLevel1 { .commentContent {
margin-left: 3.2rem; &:hover {
} .commentControlReply,
.commentControlShare,
.commentLevel2 { .commentControlDelete,
margin-left: 6.4rem; .commentControlEdit,
} .commentControlComplain {
opacity: 1;
.commentLevel3 { }
margin-left: 9.6rem; }
}
.commentLevel4 {
margin-left: 12.8rem;
}
.commentLevel5 {
margin-left: 16rem;
} }
.commentControls { .commentControls {

View File

@ -1,25 +1,32 @@
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 } from 'solid-js' import { Show, createMemo, createSignal, For } from 'solid-js'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import type { Author, Reaction as Point } from '../../graphql/types.gen' import type { Author, Reaction } from '../../graphql/types.gen'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
// import { createReaction, updateReaction, deleteReaction } from '../../stores/zine/reactions' import { createReaction, deleteReaction } from '../../stores/zine/reactions'
import MD from './MD' import MD from './MD'
import { deleteReaction } from '../../stores/zine/reactions'
import { formatDate } from '../../utils' import { formatDate } from '../../utils'
import { SharePopup } from './SharePopup' import { SharePopup } from './SharePopup'
import stylesHeader from '../Nav/Header.module.scss' import stylesHeader from '../Nav/Header.module.scss'
import Userpic from '../Author/Userpic' import Userpic from '../Author/Userpic'
import { useSession } from '../../context/session'
import { ReactionKind } from '../../graphql/types.gen'
export default (props: { type Props = {
level?: number comment: Reaction
comment: Partial<Point>
canEdit?: boolean
compact?: boolean compact?: boolean
}) => { reactions?: Reaction[]
}
export const Comment = (props: Props) => {
const [isReplyVisible, setIsReplyVisible] = createSignal(false) const [isReplyVisible, setIsReplyVisible] = createSignal(false)
const [postMessageText, setPostMessageText] = createSignal('')
const { session } = useSession()
const canEdit = createMemo(() => props.comment.createdBy?.slug === session()?.user?.slug)
const comment = createMemo(() => props.comment) const comment = createMemo(() => props.comment)
const body = createMemo(() => (comment().body || '').trim()) const body = createMemo(() => (comment().body || '').trim())
@ -29,12 +36,28 @@ export default (props: {
deleteReaction(comment().id) deleteReaction(comment().id)
} }
} }
const compose = (event) => setPostMessageText(event.target.value)
const handleCreate = async (event) => {
event.preventDefault()
try {
await createReaction({
kind: ReactionKind.Comment,
replyTo: props.comment.id,
body: postMessageText(),
shout: comment().shout.id
})
setIsReplyVisible(false)
} catch (error) {
console.log('!!! err:', error)
}
}
const formattedDate = createMemo(() => const formattedDate = createMemo(() =>
formatDate(new Date(comment()?.createdAt), { hour: 'numeric', minute: 'numeric' }) formatDate(new Date(comment()?.createdAt), { hour: 'numeric', minute: 'numeric' })
) )
return ( return (
<div class={clsx(styles.comment, { [styles[`commentLevel${props.level}`]]: Boolean(props.level) })}> <li class={styles.comment}>
<Show when={!!body()}> <Show when={!!body()}>
<div class={styles.commentContent}> <div class={styles.commentContent}>
<Show <Show
@ -73,10 +96,10 @@ export default (props: {
</div> </div>
</div> </div>
</Show> </Show>
<div style={{ color: 'red' }}>{comment().id}</div>
<div <div
class={styles.commentBody} class={styles.commentBody}
contenteditable={props.canEdit} contenteditable={canEdit()}
id={'comment-' + (comment().id || '')} id={'comment-' + (comment().id || '')}
> >
<MD body={body()} /> <MD body={body()} />
@ -92,7 +115,7 @@ export default (props: {
{t('Reply')} {t('Reply')}
</button> </button>
<Show when={props.canEdit}> <Show when={canEdit()}>
{/*FIXME implement edit comment modal*/} {/*FIXME implement edit comment modal*/}
{/*<button*/} {/*<button*/}
{/* class={clsx(styles.commentControl, styles.commentControlEdit)}*/} {/* class={clsx(styles.commentControl, styles.commentControlEdit)}*/}
@ -129,19 +152,34 @@ export default (props: {
</div> </div>
<Show when={isReplyVisible()}> <Show when={isReplyVisible()}>
<form class={styles.replyForm}> <form class={styles.replyForm} onSubmit={(event) => handleCreate(event)}>
<textarea name="reply" id="reply" rows="5" /> <textarea
value={postMessageText()}
rows={1}
onInput={(event) => compose(event)}
placeholder="Написать сообщение"
/>
<div class={styles.replyFormControls}> <div class={styles.replyFormControls}>
<button class="button button--light" onClick={() => setIsReplyVisible(false)}> <button class="button button--light" onClick={() => setIsReplyVisible(false)}>
{t('Cancel')} {t('cancel')}
</button>
<button type="submit" class="button">
{t('Send')}
</button> </button>
<button class="button">{t('Send')}</button>
</div> </div>
</form> </form>
</Show> </Show>
</Show> </Show>
</div> </div>
</Show> </Show>
</div> <Show when={props.reactions}>
<ul>
<For each={props.reactions.filter((r) => r.replyTo === props.comment.id)}>
{(reaction) => <Comment reactions={props.reactions} comment={reaction} />}
</For>
</ul>
</Show>
</li>
) )
} }

View File

@ -1,6 +1,6 @@
import { For, Show, createMemo, createSignal, onMount } from 'solid-js' import { For, Show, createMemo, createSignal, onMount, createEffect } from 'solid-js'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import Comment from './Comment' import { Comment } from './Comment'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { showModal } from '../../stores/ui' import { showModal } from '../../stores/ui'
import styles from '../../styles/Article.module.scss' import styles from '../../styles/Article.module.scss'
@ -21,16 +21,16 @@ export const CommentsTree = (props: { shoutSlug: string }) => {
const { session } = useSession() const { session } = useSession()
const { sortedReactions, loadReactionsBy } = useReactionsStore() const { sortedReactions, loadReactionsBy } = useReactionsStore()
const reactions = createMemo<Reaction[]>(() => const reactions = createMemo<Reaction[]>(() =>
sortedReactions() sortedReactions().sort(commentsOrder() === 'rating' ? byStat('rating') : byCreated)
.sort(commentsOrder() === 'rating' ? byStat('rating') : byCreated)
.filter((r) => r.shout.slug === props.shoutSlug)
) )
createEffect(() => {
console.log('!!! sortedReactions():', sortedReactions())
})
const loadMore = async () => { const loadMore = async () => {
try { try {
const page = getCommentsPage() const page = getCommentsPage()
setIsCommentsLoading(true) setIsCommentsLoading(true)
const { hasMore } = await loadReactionsBy({ const { hasMore } = await loadReactionsBy({
by: { shout: props.shoutSlug, comment: true }, by: { shout: props.shoutSlug, comment: true },
limit: ARTICLE_COMMENTS_PAGE_SIZE, limit: ARTICLE_COMMENTS_PAGE_SIZE,
@ -49,6 +49,7 @@ export const CommentsTree = (props: { shoutSlug: string }) => {
return level return level
} }
onMount(async () => await loadMore()) onMount(async () => await loadMore())
return ( return (
<> <>
<Show when={!isCommentsLoading()} fallback={<Loading />}> <Show when={!isCommentsLoading()} fallback={<Loading />}>
@ -83,15 +84,11 @@ export const CommentsTree = (props: { shoutSlug: string }) => {
</ul> </ul>
</div> </div>
<For each={reactions().reverse()}> <ul class={styles.comments}>
{(reaction: Reaction) => ( <For each={reactions().filter((r) => !r.replyTo)}>
<Comment {(reaction) => <Comment reactions={reactions()} comment={reaction} />}
comment={reaction} </For>
level={getCommentLevel(reaction)} </ul>
canEdit={reaction.createdBy?.slug === session()?.user?.slug}
/>
)}
</For>
<Show when={isLoadMoreButtonVisible()}> <Show when={isLoadMoreButtonVisible()}>
<button onClick={loadMore}>{t('Load more')}</button> <button onClick={loadMore}>{t('Load more')}</button>

View File

@ -38,11 +38,13 @@ export const AuthorCard = (props: AuthorCardProps) => {
actions: { loadSession } actions: { loadSession }
} = useSession() } = useSession()
if (!props.author) return false // FIXME: с сервера должен приходить автор реакции (ApiClient.CreateReaction)
const [isSubscribing, setIsSubscribing] = createSignal(false) const [isSubscribing, setIsSubscribing] = createSignal(false)
const subscribed = createMemo<boolean>( const subscribed = createMemo<boolean>(() => {
() => session()?.news?.authors?.some((u) => u === props.author.slug) || false return session()?.news?.authors?.some((u) => u === props.author.slug) || false
) })
const subscribe = async (really = true) => { const subscribe = async (really = true) => {
setIsSubscribing(true) setIsSubscribing(true)

View File

@ -11,8 +11,6 @@ import { useSession } from '../../context/session'
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
const log = getLogger('TopicCard')
interface TopicProps { interface TopicProps {
topic: Topic topic: Topic
compact?: boolean compact?: boolean

View File

@ -7,7 +7,7 @@ import { ArticleCard } from '../Feed/Card'
import { AuthorCard } from '../Author/Card' import { AuthorCard } from '../Author/Card'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { FeedSidebar } from '../Feed/Sidebar' import { FeedSidebar } from '../Feed/Sidebar'
import CommentCard from '../Article/Comment' import { Comment as CommentCard } from '../Article/Comment'
import { loadShouts, useArticlesStore } from '../../stores/zine/articles' import { loadShouts, useArticlesStore } from '../../stores/zine/articles'
import { useReactionsStore } from '../../stores/zine/reactions' import { useReactionsStore } from '../../stores/zine/reactions'
import { useAuthorsStore } from '../../stores/zine/authors' import { useAuthorsStore } from '../../stores/zine/authors'
@ -128,7 +128,8 @@ export const FeedView = () => {
<section class="feed-comments"> <section class="feed-comments">
<h4>{t('Comments')}</h4> <h4>{t('Comments')}</h4>
<For each={topComments()}> <For each={topComments()}>
{(comment) => <CommentCard comment={comment} compact={true} />} {/*FIXME: different components/better comment props*/}
{(comment) => <CommentCard comment={comment} level={0} reactions={[]} compact={true} />}
</For> </For>
</section> </section>
<Show when={topTopics().length > 0}> <Show when={topTopics().length > 0}>

View File

@ -6,26 +6,11 @@ export default gql`
error error
reaction { reaction {
id id
createdBy {
slug
name
userpic
}
body body
kind kind
range range
createdAt createdAt
shout replyTo
replyTo {
id
createdBy {
slug
userpic
name
}
body
kind
}
} }
} }
} }

View File

@ -8,7 +8,9 @@ export default gql`
range range
replyTo replyTo
shout { shout {
id
slug slug
title
} }
createdBy { createdBy {
name name

View File

@ -238,7 +238,7 @@ export type MutationDeleteMessageArgs = {
} }
export type MutationDeleteReactionArgs = { export type MutationDeleteReactionArgs = {
id: Scalars['Int'] reaction: Scalars['Int']
} }
export type MutationDeleteShoutArgs = { export type MutationDeleteShoutArgs = {
@ -498,10 +498,10 @@ export type ReactionBy = {
export type ReactionInput = { export type ReactionInput = {
body?: InputMaybe<Scalars['String']> body?: InputMaybe<Scalars['String']>
kind: Scalars['Int'] kind: ReactionKind
range?: InputMaybe<Scalars['String']> range?: InputMaybe<Scalars['String']>
replyTo?: InputMaybe<Scalars['Int']> replyTo?: InputMaybe<Scalars['Int']>
shout: Scalars['String'] shout: Scalars['Int']
} }
export enum ReactionKind { export enum ReactionKind {

View File

@ -189,6 +189,7 @@
"create_group": "Создать группу", "create_group": "Создать группу",
"discourse_theme": "Тема дискурса", "discourse_theme": "Тема дискурса",
"cancel": "Отмена", "cancel": "Отмена",
"Send": "Отправить",
"group_chat": "Общий чат", "group_chat": "Общий чат",
"Choose who you want to write to": "Выберите кому хотите написать", "Choose who you want to write to": "Выберите кому хотите написать",
"Start conversation": "Начать беседу", "Start conversation": "Начать беседу",

View File

@ -1,4 +1,4 @@
import type { Reaction } 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 { createSignal } from 'solid-js'
// TODO: import { roomConnect } from '../../utils/p2p' // TODO: import { roomConnect } from '../../utils/p2p'
@ -23,9 +23,16 @@ export const loadReactionsBy = async ({
setSortedReactions(data) setSortedReactions(data)
return { hasMore } return { hasMore }
} }
export const createReaction = async (reaction: Reaction) => {
const { reaction: r } = await apiClient.createReaction({ reaction }) export const createReaction = async (input: ReactionInput) => {
return r try {
const reaction = await apiClient.createReaction(input)
console.log('!!! reaction:', reaction)
reaction.shout = { slug: input.shout }
setSortedReactions((prev) => [...prev, reaction])
} catch (error) {
console.error('[createReaction]', error)
}
} }
export const updateReaction = async (reaction: Reaction) => { export const updateReaction = async (reaction: Reaction) => {
const { reaction: r } = await apiClient.updateReaction({ reaction }) const { reaction: r } = await apiClient.updateReaction({ reaction })

View File

@ -252,6 +252,20 @@ img {
} }
} }
.comments {
margin: 0;
&,
ul {
list-style: none;
padding: 0;
}
ul {
margin: 1em 0 0 2.4rem;
}
}
.commentsHeaderWrapper { .commentsHeaderWrapper {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@ -12,8 +12,9 @@ import type {
MutationCreateMessageArgs, MutationCreateMessageArgs,
Chat, Chat,
QueryLoadRecipientsArgs, QueryLoadRecipientsArgs,
User,
ProfileInput, ProfileInput,
ReactionBy ReactionInput
} from '../graphql/types.gen' } from '../graphql/types.gen'
import { publicGraphQLClient } from '../graphql/publicGraphQLClient' import { publicGraphQLClient } from '../graphql/publicGraphQLClient'
import { getToken, privateGraphQLClient } from '../graphql/privateGraphQLClient' import { getToken, privateGraphQLClient } from '../graphql/privateGraphQLClient'
@ -230,16 +231,16 @@ export const apiClient = {
console.debug('createArticle response:', response) console.debug('createArticle response:', response)
return response.data.createShout return response.data.createShout
}, },
createReaction: async ({ reaction }) => { createReaction: async (input: ReactionInput) => {
const response = await privateGraphQLClient.mutation(reactionCreate, { reaction }).toPromise() const response = await privateGraphQLClient.mutation(reactionCreate, { reaction: input }).toPromise()
console.debug('[api-client] [api] create reaction mutation called') console.debug('[createReaction]:', response.data)
return response.data.createReaction return response.data.createReaction.reaction
}, },
// CUDL // CUDL
updateReaction: async ({ reaction }) => { updateReaction: async (reaction) => {
const response = await privateGraphQLClient.mutation(reactionUpdate, { reaction }).toPromise() const response = await privateGraphQLClient.mutation(reactionUpdate, reaction).toPromise()
return response.data.createReaction return response.data.createReaction
}, },