parent
f22d6c60c5
commit
87a99d9e8d
|
@ -122,12 +122,31 @@
|
||||||
@include font-size(1.5rem);
|
@include font-size(1.5rem);
|
||||||
|
|
||||||
line-height: 1.47;
|
line-height: 1.47;
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 10px;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #9fa1a7;
|
||||||
|
border-left: 2px solid #696969;
|
||||||
|
|
||||||
|
p,
|
||||||
|
.paragraph {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.commentAuthor,
|
.commentAuthor,
|
||||||
.commentDate,
|
.commentDate,
|
||||||
.commentRating {
|
.commentRating {
|
||||||
@include font-size(1.2rem);
|
@include font-size(1.2rem);
|
||||||
}
|
}
|
||||||
|
.articleAuthor {
|
||||||
|
color: #2638d9;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
.commentDate {
|
.commentDate {
|
||||||
color: rgb(0 0 0 / 30%);
|
color: rgb(0 0 0 / 30%);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
@ -13,18 +13,20 @@ 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 GrowingTextarea from '../_shared/GrowingTextarea'
|
import CommentEditor from '../_shared/CommentEditor'
|
||||||
|
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
comment: Reaction
|
comment: Reaction
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
reactions?: Reaction[]
|
reactions?: Reaction[]
|
||||||
|
isArticleAuthor?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Comment = (props: Props) => {
|
const Comment = (props: Props) => {
|
||||||
const [isReplyVisible, setIsReplyVisible] = createSignal(false)
|
const [isReplyVisible, setIsReplyVisible] = createSignal(false)
|
||||||
const [loading, setLoading] = createSignal(false)
|
const [loading, setLoading] = createSignal<boolean>(false)
|
||||||
const [errorMessage, setErrorMessage] = createSignal<string | null>(null)
|
const [submitted, setSubmitted] = 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)
|
||||||
|
@ -58,12 +60,13 @@ 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)
|
||||||
setErrorMessage(t('Something went wrong, please try again'))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const formattedDate = createMemo(() =>
|
const formattedDate = createMemo(() =>
|
||||||
formatDate(new Date(comment()?.createdAt), { hour: 'numeric', minute: 'numeric' })
|
formatDate(new Date(comment()?.createdAt), { hour: 'numeric', minute: 'numeric' })
|
||||||
)
|
)
|
||||||
|
@ -94,6 +97,10 @@ const Comment = (props: Props) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Show when={props.isArticleAuthor}>
|
||||||
|
<div class={styles.articleAuthor}>{t('Author')}</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<div class={styles.commentDate}>{formattedDate()}</div>
|
<div class={styles.commentDate}>{formattedDate()}</div>
|
||||||
<div
|
<div
|
||||||
class={styles.commentRating}
|
class={styles.commentRating}
|
||||||
|
@ -164,14 +171,13 @@ const Comment = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={isReplyVisible()}>
|
<Show when={isReplyVisible()}>
|
||||||
<GrowingTextarea
|
<ShowOnlyOnClient>
|
||||||
placeholder={t('Write comment')}
|
<CommentEditor
|
||||||
submitButtonText={t('Send')}
|
initialValue={''}
|
||||||
cancelButtonText={t('cancel')}
|
clear={submitted()}
|
||||||
submit={(value) => handleCreate(value)}
|
onSubmit={(value) => handleCreate(value)}
|
||||||
loading={loading()}
|
|
||||||
errorMessage={errorMessage()}
|
|
||||||
/>
|
/>
|
||||||
|
</ShowOnlyOnClient>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
@ -179,7 +185,13 @@ const Comment = (props: Props) => {
|
||||||
<Show when={props.reactions}>
|
<Show when={props.reactions}>
|
||||||
<ul>
|
<ul>
|
||||||
<For each={props.reactions.filter((r) => r.replyTo === props.comment.id)}>
|
<For each={props.reactions.filter((r) => r.replyTo === props.comment.id)}>
|
||||||
{(reaction) => <Comment reactions={props.reactions} comment={reaction} />}
|
{(reaction) => (
|
||||||
|
<Comment
|
||||||
|
isArticleAuthor={props.isArticleAuthor}
|
||||||
|
reactions={props.reactions}
|
||||||
|
comment={reaction}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</For>
|
</For>
|
||||||
</ul>
|
</ul>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -7,14 +7,21 @@ import type { Reaction } from '../../graphql/types.gen'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { byCreated, byStat } from '../../utils/sortby'
|
import { byCreated, byStat } from '../../utils/sortby'
|
||||||
import { Loading } from '../Loading'
|
import { Loading } from '../Loading'
|
||||||
import GrowingTextarea from '../_shared/GrowingTextarea'
|
import { Author, ReactionKind } from '../../graphql/types.gen'
|
||||||
import { ReactionKind } from '../../graphql/types.gen'
|
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
|
import CommentEditor from '../_shared/CommentEditor'
|
||||||
|
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
||||||
|
|
||||||
const ARTICLE_COMMENTS_PAGE_SIZE = 50
|
const ARTICLE_COMMENTS_PAGE_SIZE = 50
|
||||||
const MAX_COMMENT_LEVEL = 6
|
const MAX_COMMENT_LEVEL = 6
|
||||||
|
|
||||||
export const CommentsTree = (props: { shoutSlug: string; shoutId: number }) => {
|
type Props = {
|
||||||
|
commentAuthors: Author[]
|
||||||
|
shoutSlug: string
|
||||||
|
shoutId: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommentsTree = (props: Props) => {
|
||||||
const [getCommentsPage, setCommentsPage] = createSignal(0)
|
const [getCommentsPage, setCommentsPage] = createSignal(0)
|
||||||
const [commentsOrder, setCommentsOrder] = createSignal<'rating' | 'createdAt'>('createdAt')
|
const [commentsOrder, setCommentsOrder] = createSignal<'rating' | 'createdAt'>('createdAt')
|
||||||
const [isCommentsLoading, setIsCommentsLoading] = createSignal(false)
|
const [isCommentsLoading, setIsCommentsLoading] = createSignal(false)
|
||||||
|
@ -47,11 +54,9 @@ export const CommentsTree = (props: { shoutSlug: string; shoutId: number }) => {
|
||||||
}
|
}
|
||||||
onMount(async () => await loadMore())
|
onMount(async () => await loadMore())
|
||||||
|
|
||||||
const [loading, setLoading] = createSignal<boolean>(false)
|
const [submitted, setSubmitted] = createSignal<boolean>(false)
|
||||||
const [errorMessage, setErrorMessage] = createSignal<string | null>(null)
|
|
||||||
const handleSubmitComment = async (value) => {
|
const handleSubmitComment = async (value) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
|
||||||
await createReaction(
|
await createReaction(
|
||||||
{
|
{
|
||||||
kind: ReactionKind.Comment,
|
kind: ReactionKind.Comment,
|
||||||
|
@ -64,12 +69,12 @@ export const CommentsTree = (props: { shoutSlug: string; shoutId: number }) => {
|
||||||
slug: session().user.slug
|
slug: session().user.slug
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
setLoading(false)
|
setSubmitted(true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrorMessage(t('Something went wrong, please try again'))
|
|
||||||
console.error('[handleCreate reaction]:', error)
|
console.error('[handleCreate reaction]:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Show when={!isCommentsLoading()} fallback={<Loading />}>
|
<Show when={!isCommentsLoading()} fallback={<Loading />}>
|
||||||
|
@ -109,20 +114,25 @@ export const CommentsTree = (props: { shoutSlug: string; shoutId: number }) => {
|
||||||
.reverse()
|
.reverse()
|
||||||
.filter((r) => !r.replyTo)}
|
.filter((r) => !r.replyTo)}
|
||||||
>
|
>
|
||||||
{(reaction) => <Comment reactions={reactions()} comment={reaction} />}
|
{(reaction) => (
|
||||||
|
<Comment
|
||||||
|
isArticleAuthor={Boolean(props.commentAuthors.some((a) => a.slug === session()?.user.slug))}
|
||||||
|
reactions={reactions()}
|
||||||
|
comment={reaction}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</For>
|
</For>
|
||||||
</ul>
|
</ul>
|
||||||
<Show when={isLoadMoreButtonVisible()}>
|
<Show when={isLoadMoreButtonVisible()}>
|
||||||
<button onClick={loadMore}>{t('Load more')}</button>
|
<button onClick={loadMore}>{t('Load more')}</button>
|
||||||
</Show>
|
</Show>
|
||||||
<GrowingTextarea
|
<ShowOnlyOnClient>
|
||||||
placeholder={t('Write comment')}
|
<CommentEditor
|
||||||
submitButtonText={t('Send')}
|
initialValue={t('Write a comment...')}
|
||||||
cancelButtonText={t('cancel')}
|
clear={submitted()}
|
||||||
submit={(value) => handleSubmitComment(value)}
|
onSubmit={(value) => handleSubmitComment(value)}
|
||||||
loading={loading()}
|
|
||||||
errorMessage={errorMessage()}
|
|
||||||
/>
|
/>
|
||||||
|
</ShowOnlyOnClient>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -224,7 +224,11 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
<CommentsTree shoutSlug={props.article?.slug} shoutId={props.article?.id} />
|
<CommentsTree
|
||||||
|
shoutId={props.article?.id}
|
||||||
|
shoutSlug={props.article?.slug}
|
||||||
|
commentAuthors={props.article?.authors}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -35,7 +35,7 @@ const CreateModalContent = (props: Props) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
if (usersId().length > 1 && theme().length === 1) {
|
if (usersId().length > 1 && theme().length === 1) {
|
||||||
setTheme(t('group_chat'))
|
setTheme(t('Group Chat'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,12 @@ import GroupDialogAvatar from './GroupDialogAvatar'
|
||||||
import formattedTime from '../../utils/formatDateTime'
|
import formattedTime from '../../utils/formatDateTime'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import styles from './DialogCard.module.scss'
|
import styles from './DialogCard.module.scss'
|
||||||
|
import { t } from '../../utils/intl'
|
||||||
|
|
||||||
type DialogProps = {
|
type DialogProps = {
|
||||||
online?: boolean
|
online?: boolean
|
||||||
message?: string
|
message?: string
|
||||||
counter?: number
|
counter?: number
|
||||||
title?: string
|
|
||||||
ownId: number
|
ownId: number
|
||||||
members: ChatMember[]
|
members: ChatMember[]
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
|
@ -23,6 +23,7 @@ const DialogCard = (props: DialogProps) => {
|
||||||
const companions = createMemo(
|
const companions = createMemo(
|
||||||
() => props.members && props.members.filter((member) => member.id !== props.ownId)
|
() => props.members && props.members.filter((member) => member.id !== props.ownId)
|
||||||
)
|
)
|
||||||
|
|
||||||
const names = createMemo(() =>
|
const names = createMemo(() =>
|
||||||
companions()
|
companions()
|
||||||
?.map((companion) => companion.name)
|
?.map((companion) => companion.name)
|
||||||
|
@ -40,14 +41,16 @@ const DialogCard = (props: DialogProps) => {
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
>
|
>
|
||||||
<div class={styles.avatar}>
|
<div class={styles.avatar}>
|
||||||
<Switch fallback={<DialogAvatar name={props.members[0].name} url={props.members[0].userpic} />}>
|
<Switch fallback={<DialogAvatar name={props.members[0].slug} url={props.members[0].userpic} />}>
|
||||||
<Match when={props.members.length >= 3}>
|
<Match when={props.members.length >= 3}>
|
||||||
<GroupDialogAvatar users={props.members} />
|
<GroupDialogAvatar users={props.members} />
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.row}>
|
<div class={styles.row}>
|
||||||
<div class={styles.name}>{props.title}</div>
|
<div class={styles.name}>
|
||||||
|
{companions()?.length > 1 ? t('Group Chat') : companions()[0]?.name}
|
||||||
|
</div>
|
||||||
<div class={styles.message}>
|
<div class={styles.message}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.message && !props.isChatHeader}>{props.message}</Match>
|
<Match when={props.message && !props.isChatHeader}>{props.message}</Match>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import MessagesFallback from '../Inbox/MessagesFallback'
|
||||||
import QuotedMessage from '../Inbox/QuotedMessage'
|
import QuotedMessage from '../Inbox/QuotedMessage'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import { loadMessages, loadRecipients } from '../../stores/inbox'
|
import { loadRecipients } from '../../stores/inbox'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { Modal } from '../Nav/Modal'
|
import { Modal } from '../Nav/Modal'
|
||||||
import { showModal } from '../../stores/ui'
|
import { showModal } from '../../stores/ui'
|
||||||
|
@ -155,7 +155,7 @@ export const InboxView = () => {
|
||||||
return messages().find((message) => message.id === messageId)
|
return messages().find((message) => message.id === messageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (event) => {
|
const handleKeyDown = async (event) => {
|
||||||
if (event.keyCode === 13 && event.shiftKey) return
|
if (event.keyCode === 13 && event.shiftKey) return
|
||||||
if (event.keyCode === 13 && !event.shiftKey && postMessageText().trim().length > 0) {
|
if (event.keyCode === 13 && !event.shiftKey && postMessageText().trim().length > 0) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
@ -217,7 +217,6 @@ export const InboxView = () => {
|
||||||
<DialogCard
|
<DialogCard
|
||||||
onClick={() => handleOpenChat(chat)}
|
onClick={() => handleOpenChat(chat)}
|
||||||
isOpened={chat.id === currentDialog()?.id}
|
isOpened={chat.id === currentDialog()?.id}
|
||||||
title={chat.title || chat.members[0].name}
|
|
||||||
members={chat.members}
|
members={chat.members}
|
||||||
ownId={currentUserId()}
|
ownId={currentUserId()}
|
||||||
lastUpdate={chat.updatedAt}
|
lastUpdate={chat.updatedAt}
|
||||||
|
|
91
src/components/_shared/CommentEditor/CommentEditor.tsx
Normal file
91
src/components/_shared/CommentEditor/CommentEditor.tsx
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import styles from './styles/CommentEditor.module.scss'
|
||||||
|
import './styles/ProseMirrorOverrides.scss'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import Button from '../Button'
|
||||||
|
import { createEffect, createMemo, onMount } from 'solid-js'
|
||||||
|
import { t } from '../../../utils/intl'
|
||||||
|
//ProseMirror deps
|
||||||
|
import { schema } from './schema'
|
||||||
|
import { EditorState } from 'prosemirror-state'
|
||||||
|
import { EditorView } from 'prosemirror-view'
|
||||||
|
import { DOMSerializer } from 'prosemirror-model'
|
||||||
|
import { renderGrouped } from 'prosemirror-menu'
|
||||||
|
import { buildMenuItems } from './menu'
|
||||||
|
import { keymap } from 'prosemirror-keymap'
|
||||||
|
import { baseKeymap } from 'prosemirror-commands'
|
||||||
|
import { customKeymap } from '../../EditorNew/prosemirror/plugins/customKeymap'
|
||||||
|
import { placeholder } from '../../EditorNew/prosemirror/plugins/placeholder'
|
||||||
|
import { undo, redo, history } from 'prosemirror-history'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
initialValue: string
|
||||||
|
onSubmit: (value: string) => void
|
||||||
|
clear?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlContainer = typeof document === 'undefined' ? null : document.createElement('div')
|
||||||
|
const getHtml = (state: EditorState) => {
|
||||||
|
const fragment = DOMSerializer.fromSchema(schema).serializeFragment(state.doc.content)
|
||||||
|
htmlContainer.replaceChildren(fragment)
|
||||||
|
return htmlContainer.innerHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommentEditor = (props: Props) => {
|
||||||
|
const editorElRef: { current: HTMLDivElement } = { current: null }
|
||||||
|
const menuElRef: { current: HTMLDivElement } = { current: null }
|
||||||
|
const editorViewRef: { current: EditorView } = { current: null }
|
||||||
|
const initEditor = () => {
|
||||||
|
editorViewRef.current = new EditorView(editorElRef.current, {
|
||||||
|
state: EditorState.create({
|
||||||
|
schema,
|
||||||
|
plugins: [
|
||||||
|
history(),
|
||||||
|
customKeymap(),
|
||||||
|
placeholder(props.initialValue),
|
||||||
|
keymap({ 'Mod-z': undo, 'Mod-y': redo }),
|
||||||
|
keymap(baseKeymap)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
initEditor()
|
||||||
|
const { dom } = renderGrouped(editorViewRef.current, buildMenuItems(schema))
|
||||||
|
menuElRef.current.appendChild(dom)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSubmitButtonClick = () => {
|
||||||
|
props.onSubmit(getHtml(editorViewRef.current.state))
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearEditor = () => {
|
||||||
|
editorViewRef.current.destroy()
|
||||||
|
initEditor()
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.clear) clearEditor()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class={styles.commentEditor}>
|
||||||
|
<div
|
||||||
|
class={clsx('ProseMirrorOverrides', styles.textarea)}
|
||||||
|
ref={(el) => (editorElRef.current = el)}
|
||||||
|
/>
|
||||||
|
<div class={styles.actions}>
|
||||||
|
<div class={styles.menu} ref={(el) => (menuElRef.current = el)} />
|
||||||
|
<div class={styles.buttons}>
|
||||||
|
<Button value={t('Send')} variant="primary" onClick={handleSubmitButtonClick} />
|
||||||
|
<Button value="Cancel" variant="secondary" onClick={clearEditor} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class={styles.helpText}>{'"Cmd-Z": Undo, "Cmd-Y": Redo'}</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommentEditor
|
1
src/components/_shared/CommentEditor/index.ts
Normal file
1
src/components/_shared/CommentEditor/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './CommentEditor'
|
72
src/components/_shared/CommentEditor/menu/index.ts
Normal file
72
src/components/_shared/CommentEditor/menu/index.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import { blockTypeItem, icons, MenuItem, wrapItem } from 'prosemirror-menu'
|
||||||
|
import { toggleMark } from 'prosemirror-commands'
|
||||||
|
|
||||||
|
const markActive = (state, type) => {
|
||||||
|
const { from, $from, to, empty } = state.selection
|
||||||
|
|
||||||
|
if (empty) return type.isInSet(state.storedMarks || $from.marks())
|
||||||
|
|
||||||
|
return state.doc.rangeHasMark(from, to, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmdItem = (cmd, options) => {
|
||||||
|
const passedOptions = {
|
||||||
|
label: options.title,
|
||||||
|
run: cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const prop in options) passedOptions[prop] = options[prop]
|
||||||
|
|
||||||
|
if ((!options.enable || options.enable === true) && !options.select) {
|
||||||
|
passedOptions[options.enable ? 'enable' : 'select'] = (state) => cmd(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MenuItem(passedOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
const markItem = (markType, options) => {
|
||||||
|
const passedOptions = {
|
||||||
|
active(state) {
|
||||||
|
return markActive(state, markType)
|
||||||
|
},
|
||||||
|
enable: true
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const prop in options) passedOptions[prop] = options[prop]
|
||||||
|
|
||||||
|
return cmdItem(toggleMark(markType), passedOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: вывести тип для схемы
|
||||||
|
export const buildMenuItems = (schema) => {
|
||||||
|
const toggleStrong = markItem(schema.marks.strong, {
|
||||||
|
title: 'Toggle strong style',
|
||||||
|
icon: {
|
||||||
|
width: 14,
|
||||||
|
height: 16,
|
||||||
|
path: 'M9.82857 7.76C10.9371 6.99429 11.7143 5.73714 11.7143 4.57143C11.7143 1.98857 9.71428 0 7.14286 0H0V16H8.04571C10.4343 16 12.2857 14.0571 12.2857 11.6686C12.2857 9.93143 11.3029 8.44571 9.82857 7.76ZM3.42799 2.85708H6.85656C7.80513 2.85708 8.57085 3.6228 8.57085 4.57137C8.57085 5.51994 7.80513 6.28565 6.85656 6.28565H3.42799V2.85708ZM3.42799 13.1429H7.42799C8.37656 13.1429 9.14228 12.3772 9.14228 11.4286C9.14228 10.4801 8.37656 9.71434 7.42799 9.71434H3.42799V13.1429Z'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleEm = markItem(schema.marks.em, {
|
||||||
|
title: 'Toggle emphasis',
|
||||||
|
icon: {
|
||||||
|
width: 14,
|
||||||
|
height: 16,
|
||||||
|
path: 'M4.39216 0V3.42857H6.81882L3.06353 12.5714H0V16H8.78431V12.5714H6.35765L10.1129 3.42857H13.1765V0H4.39216Z'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// const toggleLink = linkItem(schema.marks.link)
|
||||||
|
|
||||||
|
// const insertImage = insertImageItem(schema.nodes.image)
|
||||||
|
|
||||||
|
const wrapBlockQuote = wrapItem(schema.nodes.blockquote, {
|
||||||
|
title: 'Wrap in block quote',
|
||||||
|
icon: icons.blockquote
|
||||||
|
})
|
||||||
|
|
||||||
|
const inlineMenu = [toggleStrong, toggleEm, wrapBlockQuote]
|
||||||
|
|
||||||
|
return [inlineMenu]
|
||||||
|
}
|
21
src/components/_shared/CommentEditor/plugins/placeholder.ts
Normal file
21
src/components/_shared/CommentEditor/plugins/placeholder.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { Plugin } from 'prosemirror-state'
|
||||||
|
import { DecorationSet, Decoration } from 'prosemirror-view'
|
||||||
|
|
||||||
|
export const placeholder = (text: string): Plugin =>
|
||||||
|
new Plugin({
|
||||||
|
props: {
|
||||||
|
decorations(state) {
|
||||||
|
const { doc } = state
|
||||||
|
|
||||||
|
if (doc.childCount > 1 || !doc.firstChild.isTextblock || doc.firstChild.content.size > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.setAttribute('contenteditable', 'false')
|
||||||
|
div.textContent = text
|
||||||
|
|
||||||
|
return DecorationSet.create(doc, [Decoration.widget(1, div)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
43
src/components/_shared/CommentEditor/schema.ts
Normal file
43
src/components/_shared/CommentEditor/schema.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { Schema } from 'prosemirror-model'
|
||||||
|
|
||||||
|
export const schema = new Schema({
|
||||||
|
nodes: {
|
||||||
|
doc: {
|
||||||
|
content: 'block+'
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
group: 'inline',
|
||||||
|
inline: true
|
||||||
|
},
|
||||||
|
paragraph: {
|
||||||
|
content: 'inline*',
|
||||||
|
group: 'block',
|
||||||
|
toDOM: function toDOM(node) {
|
||||||
|
return ['p', { class: 'paragraph' }, 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
blockquote: {
|
||||||
|
content: 'block+',
|
||||||
|
group: 'block',
|
||||||
|
defining: true,
|
||||||
|
parseDOM: [{ tag: 'blockquote' }],
|
||||||
|
toDOM() {
|
||||||
|
return ['blockquote', 0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
marks: {
|
||||||
|
strong: {
|
||||||
|
toDOM() {
|
||||||
|
return ['strong', 0]
|
||||||
|
},
|
||||||
|
parseDOM: [{ tag: 'strong' }, { tag: 'b' }, { style: 'font-weight=bold' }]
|
||||||
|
},
|
||||||
|
em: {
|
||||||
|
toDOM() {
|
||||||
|
return ['em', 0]
|
||||||
|
},
|
||||||
|
parseDOM: [{ tag: 'em' }, { tag: 'i' }, { style: 'font-style=italic' }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,32 @@
|
||||||
|
.commentEditor {
|
||||||
|
border: 2px solid #e8e8e8;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
min-height: 1em;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.menu,
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpText {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #696969;
|
||||||
|
margin: 12px 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
.ProseMirrorOverrides > .ProseMirror {
|
||||||
|
.paragraph {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
padding-left: 10px;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #9fa1a7;
|
||||||
|
border-left: 2px solid #696969;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +0,0 @@
|
||||||
export { default } from './GrowingTextarea'
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createContext, createSignal, useContext } from 'solid-js'
|
import { createContext, createEffect, createSignal, useContext } from 'solid-js'
|
||||||
import type { Accessor, JSX } from 'solid-js'
|
import type { Accessor, JSX } from 'solid-js'
|
||||||
// import { createChatClient } from '../graphql/privateGraphQLClient'
|
// import { createChatClient } from '../graphql/privateGraphQLClient'
|
||||||
import type { Chat, Message, MutationCreateMessageArgs } from '../graphql/types.gen'
|
import type { Chat, Message, MutationCreateMessageArgs } from '../graphql/types.gen'
|
||||||
|
|
|
@ -6,6 +6,10 @@ export default gql`
|
||||||
error
|
error
|
||||||
chat {
|
chat {
|
||||||
id
|
id
|
||||||
|
members {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ export default gql`
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
userpic
|
userpic
|
||||||
|
online
|
||||||
}
|
}
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,14 +84,6 @@ export type ChatMember = {
|
||||||
userpic?: Maybe<Scalars['String']>
|
userpic?: Maybe<Scalars['String']>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Collab = {
|
|
||||||
authors: Array<Maybe<Scalars['String']>>
|
|
||||||
chat?: Maybe<Chat>
|
|
||||||
createdAt: Scalars['Int']
|
|
||||||
invites?: Maybe<Array<Maybe<Scalars['String']>>>
|
|
||||||
shout?: Maybe<Shout>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Collection = {
|
export type Collection = {
|
||||||
amount?: Maybe<Scalars['Int']>
|
amount?: Maybe<Scalars['Int']>
|
||||||
createdAt: Scalars['DateTime']
|
createdAt: Scalars['DateTime']
|
||||||
|
@ -113,6 +105,30 @@ export type Community = {
|
||||||
slug: Scalars['String']
|
slug: Scalars['String']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DraftCollab = {
|
||||||
|
authors: Array<Maybe<Scalars['Int']>>
|
||||||
|
body?: Maybe<Scalars['String']>
|
||||||
|
chat?: Maybe<Chat>
|
||||||
|
cover?: Maybe<Scalars['String']>
|
||||||
|
createdAt: Scalars['Int']
|
||||||
|
layout?: Maybe<Scalars['String']>
|
||||||
|
slug?: Maybe<Scalars['String']>
|
||||||
|
subtitle?: Maybe<Scalars['String']>
|
||||||
|
title?: Maybe<Scalars['String']>
|
||||||
|
topics?: Maybe<Array<Maybe<Scalars['String']>>>
|
||||||
|
updatedAt?: Maybe<Scalars['Int']>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DraftInput = {
|
||||||
|
authors?: InputMaybe<Array<InputMaybe<Scalars['Int']>>>
|
||||||
|
body?: InputMaybe<Scalars['String']>
|
||||||
|
cover?: InputMaybe<Scalars['String']>
|
||||||
|
slug?: InputMaybe<Scalars['String']>
|
||||||
|
subtitle?: InputMaybe<Scalars['String']>
|
||||||
|
title?: InputMaybe<Scalars['String']>
|
||||||
|
topics?: InputMaybe<Array<InputMaybe<Scalars['Int']>>>
|
||||||
|
}
|
||||||
|
|
||||||
export enum FollowingEntity {
|
export enum FollowingEntity {
|
||||||
Author = 'AUTHOR',
|
Author = 'AUTHOR',
|
||||||
Community = 'COMMUNITY',
|
Community = 'COMMUNITY',
|
||||||
|
@ -167,28 +183,30 @@ export type MessagesBy = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Mutation = {
|
export type Mutation = {
|
||||||
acceptCoauthor: Result
|
|
||||||
confirmEmail: AuthResult
|
confirmEmail: AuthResult
|
||||||
createChat: Result
|
createChat: Result
|
||||||
|
createDraft: Result
|
||||||
createMessage: Result
|
createMessage: Result
|
||||||
createReaction: Result
|
createReaction: Result
|
||||||
createShout: Result
|
createShout: Result
|
||||||
createTopic: Result
|
createTopic: Result
|
||||||
deleteChat: Result
|
deleteChat: Result
|
||||||
|
deleteDraft: Result
|
||||||
deleteMessage: Result
|
deleteMessage: Result
|
||||||
deleteReaction: Result
|
deleteReaction: Result
|
||||||
deleteShout: Result
|
deleteShout: Result
|
||||||
destroyTopic: Result
|
destroyTopic: Result
|
||||||
follow: Result
|
follow: Result
|
||||||
getSession: AuthResult
|
getSession: AuthResult
|
||||||
inviteCoauthor: Result
|
inviteAccept: Result
|
||||||
|
inviteAuthor: Result
|
||||||
markAsRead: Result
|
markAsRead: Result
|
||||||
rateUser: Result
|
rateUser: Result
|
||||||
registerUser: AuthResult
|
registerUser: AuthResult
|
||||||
removeCoauthor: Result
|
|
||||||
sendLink: Result
|
sendLink: Result
|
||||||
unfollow: Result
|
unfollow: Result
|
||||||
updateChat: Result
|
updateChat: Result
|
||||||
|
updateDraft: Result
|
||||||
updateMessage: Result
|
updateMessage: Result
|
||||||
updateOnlineStatus: Result
|
updateOnlineStatus: Result
|
||||||
updateProfile: Result
|
updateProfile: Result
|
||||||
|
@ -197,10 +215,6 @@ export type Mutation = {
|
||||||
updateTopic: Result
|
updateTopic: Result
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationAcceptCoauthorArgs = {
|
|
||||||
shout: Scalars['Int']
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MutationConfirmEmailArgs = {
|
export type MutationConfirmEmailArgs = {
|
||||||
token: Scalars['String']
|
token: Scalars['String']
|
||||||
}
|
}
|
||||||
|
@ -210,6 +224,10 @@ export type MutationCreateChatArgs = {
|
||||||
title?: InputMaybe<Scalars['String']>
|
title?: InputMaybe<Scalars['String']>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MutationCreateDraftArgs = {
|
||||||
|
draft: DraftInput
|
||||||
|
}
|
||||||
|
|
||||||
export type MutationCreateMessageArgs = {
|
export type MutationCreateMessageArgs = {
|
||||||
body: Scalars['String']
|
body: Scalars['String']
|
||||||
chat: Scalars['String']
|
chat: Scalars['String']
|
||||||
|
@ -232,6 +250,10 @@ export type MutationDeleteChatArgs = {
|
||||||
chatId: Scalars['String']
|
chatId: Scalars['String']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MutationDeleteDraftArgs = {
|
||||||
|
draft: Scalars['Int']
|
||||||
|
}
|
||||||
|
|
||||||
export type MutationDeleteMessageArgs = {
|
export type MutationDeleteMessageArgs = {
|
||||||
chatId: Scalars['String']
|
chatId: Scalars['String']
|
||||||
id: Scalars['Int']
|
id: Scalars['Int']
|
||||||
|
@ -254,9 +276,13 @@ export type MutationFollowArgs = {
|
||||||
what: FollowingEntity
|
what: FollowingEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationInviteCoauthorArgs = {
|
export type MutationInviteAcceptArgs = {
|
||||||
author: Scalars['String']
|
draft: Scalars['Int']
|
||||||
shout: Scalars['Int']
|
}
|
||||||
|
|
||||||
|
export type MutationInviteAuthorArgs = {
|
||||||
|
author: Scalars['Int']
|
||||||
|
draft: Scalars['Int']
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationMarkAsReadArgs = {
|
export type MutationMarkAsReadArgs = {
|
||||||
|
@ -275,11 +301,6 @@ export type MutationRegisterUserArgs = {
|
||||||
password?: InputMaybe<Scalars['String']>
|
password?: InputMaybe<Scalars['String']>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationRemoveCoauthorArgs = {
|
|
||||||
author: Scalars['String']
|
|
||||||
shout: Scalars['Int']
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MutationSendLinkArgs = {
|
export type MutationSendLinkArgs = {
|
||||||
email: Scalars['String']
|
email: Scalars['String']
|
||||||
lang?: InputMaybe<Scalars['String']>
|
lang?: InputMaybe<Scalars['String']>
|
||||||
|
@ -295,6 +316,10 @@ export type MutationUpdateChatArgs = {
|
||||||
chat: ChatInput
|
chat: ChatInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MutationUpdateDraftArgs = {
|
||||||
|
draft: DraftInput
|
||||||
|
}
|
||||||
|
|
||||||
export type MutationUpdateMessageArgs = {
|
export type MutationUpdateMessageArgs = {
|
||||||
body: Scalars['String']
|
body: Scalars['String']
|
||||||
chatId: Scalars['String']
|
chatId: Scalars['String']
|
||||||
|
@ -345,11 +370,11 @@ export type ProfileInput = {
|
||||||
export type Query = {
|
export type Query = {
|
||||||
authorsAll: Array<Maybe<Author>>
|
authorsAll: Array<Maybe<Author>>
|
||||||
getAuthor?: Maybe<User>
|
getAuthor?: Maybe<User>
|
||||||
getCollabs: Array<Maybe<Collab>>
|
|
||||||
getTopic?: Maybe<Topic>
|
getTopic?: Maybe<Topic>
|
||||||
isEmailUsed: Scalars['Boolean']
|
isEmailUsed: Scalars['Boolean']
|
||||||
loadAuthorsBy: Array<Maybe<Author>>
|
loadAuthorsBy: Array<Maybe<Author>>
|
||||||
loadChats: Result
|
loadChats: Result
|
||||||
|
loadDrafts: Array<Maybe<DraftCollab>>
|
||||||
loadMessagesBy: Result
|
loadMessagesBy: Result
|
||||||
loadReactionsBy: Array<Maybe<Reaction>>
|
loadReactionsBy: Array<Maybe<Reaction>>
|
||||||
loadRecipients: Result
|
loadRecipients: Result
|
||||||
|
@ -545,6 +570,7 @@ export type Result = {
|
||||||
chats?: Maybe<Array<Maybe<Chat>>>
|
chats?: Maybe<Array<Maybe<Chat>>>
|
||||||
communities?: Maybe<Array<Maybe<Community>>>
|
communities?: Maybe<Array<Maybe<Community>>>
|
||||||
community?: Maybe<Community>
|
community?: Maybe<Community>
|
||||||
|
drafts?: Maybe<Array<Maybe<DraftCollab>>>
|
||||||
error?: Maybe<Scalars['String']>
|
error?: Maybe<Scalars['String']>
|
||||||
members?: Maybe<Array<Maybe<ChatMember>>>
|
members?: Maybe<Array<Maybe<ChatMember>>>
|
||||||
message?: Maybe<Message>
|
message?: Maybe<Message>
|
||||||
|
|
|
@ -166,6 +166,7 @@
|
||||||
"actions": "действия",
|
"actions": "действия",
|
||||||
"all topics": "все темы",
|
"all topics": "все темы",
|
||||||
"author": "автор",
|
"author": "автор",
|
||||||
|
"Author": "Автор",
|
||||||
"authors": "авторы",
|
"authors": "авторы",
|
||||||
"collections": "коллекции",
|
"collections": "коллекции",
|
||||||
"community": "сообщество",
|
"community": "сообщество",
|
||||||
|
@ -192,7 +193,7 @@
|
||||||
"discourse_theme": "Тема дискурса",
|
"discourse_theme": "Тема дискурса",
|
||||||
"cancel": "Отмена",
|
"cancel": "Отмена",
|
||||||
"Send": "Отправить",
|
"Send": "Отправить",
|
||||||
"group_chat": "Общий чат",
|
"Group Chat": "Общий чат",
|
||||||
"Choose who you want to write to": "Выберите кому хотите написать",
|
"Choose who you want to write to": "Выберите кому хотите написать",
|
||||||
"Start conversation": "Начать беседу",
|
"Start conversation": "Начать беседу",
|
||||||
"Profile settings": "Настройки профиля",
|
"Profile settings": "Настройки профиля",
|
||||||
|
@ -218,6 +219,7 @@
|
||||||
"It does not look like url": "Это не похоже на ссылку",
|
"It does not look like url": "Это не похоже на ссылку",
|
||||||
"Something went wrong, please try again": "Что-то пошло не так, попробуйте еще раз",
|
"Something went wrong, please try again": "Что-то пошло не так, попробуйте еще раз",
|
||||||
"To write a comment, you must": "Чтобы написать комментарий, необходимо",
|
"To write a comment, you must": "Чтобы написать комментарий, необходимо",
|
||||||
|
"Write a comment...": "Написать комментарий..."
|
||||||
"Add comment": "Комментировать",
|
"Add comment": "Комментировать",
|
||||||
"My subscriptions": "Подписки"
|
"My subscriptions": "Подписки"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Reaction, ReactionInput, User } 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'
|
||||||
|
|
|
@ -296,12 +296,12 @@ export const apiClient = {
|
||||||
|
|
||||||
createMessage: async (options: MutationCreateMessageArgs) => {
|
createMessage: async (options: MutationCreateMessageArgs) => {
|
||||||
const resp = await privateGraphQLClient.mutation(createMessage, options).toPromise()
|
const resp = await privateGraphQLClient.mutation(createMessage, options).toPromise()
|
||||||
return resp.data.createMessage
|
return resp.data.createMessage.message
|
||||||
},
|
},
|
||||||
|
|
||||||
getChatMessages: async (options: QueryLoadMessagesByArgs) => {
|
getChatMessages: async (options: QueryLoadMessagesByArgs) => {
|
||||||
const resp = await privateGraphQLClient.query(chatMessagesLoadBy, options).toPromise()
|
const resp = await privateGraphQLClient.query(chatMessagesLoadBy, options).toPromise()
|
||||||
return resp.data.loadChat
|
return resp.data.loadMessagesBy.messages
|
||||||
},
|
},
|
||||||
getRecipients: async (options: QueryLoadRecipientsArgs) => {
|
getRecipients: async (options: QueryLoadRecipientsArgs) => {
|
||||||
const resp = await privateGraphQLClient.query(loadRecipients, options).toPromise()
|
const resp = await privateGraphQLClient.query(loadRecipients, options).toPromise()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user