parent
2e74624240
commit
0b70289195
|
@ -12,7 +12,7 @@ const getDevCssClassPrefix = (filename: string): string => {
|
|||
return filename
|
||||
.slice(filename.indexOf(PATH_PREFIX) + PATH_PREFIX.length)
|
||||
.replace('.module.scss', '')
|
||||
.replace(/[/?\\]/g, '-')
|
||||
.replaceAll(/[/?\\]/g, '-')
|
||||
}
|
||||
|
||||
const devGenerateScopedName = (name: string, filename: string, css: string) =>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
overwrite: true
|
||||
schema: 'http://localhost:8080'
|
||||
#schema: 'http://localhost:8080'
|
||||
schema: 'https://v2.discours.io'
|
||||
generates:
|
||||
src/graphql/introspec.gen.ts:
|
||||
plugins:
|
||||
|
|
|
@ -84,6 +84,7 @@
|
|||
"eslint-plugin-sonarjs": "^0.16.0",
|
||||
"eslint-plugin-unicorn": "^45.0.0",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-sse": "^1.3.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-ws": "^5.11.2",
|
||||
"hast-util-select": "^5.0.2",
|
||||
|
@ -140,7 +141,6 @@
|
|||
"unique-names-generator": "^4.7.1",
|
||||
"uuid": "^9.0.0",
|
||||
"vite": "^3.2.4",
|
||||
"vite-plugin-html-purgecss": "^0.1.1",
|
||||
"ws": "^8.11.0",
|
||||
"y-prosemirror": "^1.2.0",
|
||||
"y-protocols": "^1.0.5",
|
||||
|
|
12034
pnpm-lock.yaml
Normal file
12034
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
4
public/icons/chat-reply.svg
Normal file
4
public/icons/chat-reply.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.1746 7.75L7.84131 13.25L15.1746 18.75L15.1746 7.75Z" fill="#9FA1A7"/>
|
||||
<path d="M23.4255 24.2497C26.6333 15.0828 19.7596 10.5 15.1764 10.9573C15.1766 12.79 15.1764 15.5414 15.1764 15.5414C16.0922 15.5414 22.508 15.9997 23.4255 24.2497Z" fill="#9FA1A7"/>
|
||||
</svg>
|
After Width: | Height: | Size: 369 B |
3
public/icons/close-gray.svg
Normal file
3
public/icons/close-gray.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 18.4444L25.4444 13L27 14.5556L21.5556 20L27 25.4444L25.4444 27L20 21.5556L14.5556 27L13 25.4444L18.4444 20L13 14.5556L14.5556 13L20 18.4444Z" fill="#9FA1A7"/>
|
||||
</svg>
|
After Width: | Height: | Size: 315 B |
5
public/icons/menu.svg
Normal file
5
public/icons/menu.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 20C17.1046 20 18 20.8954 18 22C18 23.1046 17.1046 24 16 24C14.8954 24 14 23.1046 14 22C14 20.8954 14.8954 20 16 20Z" fill="#9FA1A7"/>
|
||||
<path d="M16 14C17.1046 14 18 14.8954 18 16C18 17.1046 17.1046 18 16 18C14.8954 18 14 17.1046 14 16C14 14.8954 14.8954 14 16 14Z" fill="#9FA1A7"/>
|
||||
<path d="M16 8C17.1046 8 18 8.89543 18 10C18 11.1046 17.1046 12 16 12C14.8954 12 14 11.1046 14 10C14 8.89543 14.8954 8 16 8Z" fill="#9FA1A7"/>
|
||||
</svg>
|
After Width: | Height: | Size: 540 B |
|
@ -54,6 +54,7 @@
|
|||
|
||||
.commentControls {
|
||||
@include font-size(1.2rem);
|
||||
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
|
@ -120,6 +121,7 @@
|
|||
|
||||
.commentBody {
|
||||
@include font-size(1.5rem);
|
||||
|
||||
line-height: 1.47;
|
||||
}
|
||||
|
||||
|
@ -196,6 +198,7 @@
|
|||
|
||||
button {
|
||||
@include font-size(1.6rem);
|
||||
|
||||
margin-left: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import styles from './RatingControl.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
|
||||
interface RatingControlProps {
|
||||
rating?: number
|
||||
|
|
|
@ -9,7 +9,7 @@ type SharePopupProps = Omit<PopupProps, 'children'>
|
|||
|
||||
export const SharePopup = (props: SharePopupProps) => {
|
||||
return (
|
||||
<Popup {...props}>
|
||||
<Popup {...props} variant="bordered">
|
||||
<ul class="nodash">
|
||||
<li>
|
||||
<a href="#">
|
||||
|
|
|
@ -265,6 +265,7 @@
|
|||
.authorComments {
|
||||
.authorName {
|
||||
@include font-size(1.2rem);
|
||||
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,8 +10,10 @@ import { follow, unfollow } from '../../stores/zine/common'
|
|||
import { clsx } from 'clsx'
|
||||
import { useSession } from '../../context/session'
|
||||
import { StatMetrics } from '../_shared/StatMetrics'
|
||||
import { FollowingEntity } from '../../graphql/types.gen'
|
||||
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
||||
import { FollowingEntity } from '../../graphql/types.gen'
|
||||
import { router, useRouter } from '../../stores/router'
|
||||
import { openPage } from '@nanostores/router'
|
||||
|
||||
interface AuthorCardProps {
|
||||
caption?: string
|
||||
|
@ -68,6 +70,11 @@ export const AuthorCard = (props: AuthorCardProps) => {
|
|||
})
|
||||
|
||||
// TODO: reimplement AuthorCard
|
||||
const { changeSearchParam } = useRouter()
|
||||
const initChat = () => {
|
||||
openPage(router, `inbox`)
|
||||
changeSearchParam('initChat', `${props.author.id}`)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
class={clsx(styles.author)}
|
||||
|
@ -163,6 +170,7 @@ export const AuthorCard = (props: AuthorCardProps) => {
|
|||
'button--subscribe-topic': props.isAuthorsList,
|
||||
[styles.buttonWrite]: props.liteButtons && props.isAuthorsList
|
||||
}}
|
||||
onClick={initChat}
|
||||
>
|
||||
<Icon name="comment" class={styles.icon} />
|
||||
<Show when={!props.liteButtons}>{t('Write')}</Show>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.CreateModalContent {
|
||||
padding: 24px;
|
||||
|
||||
.footer {
|
||||
padding-top: 12px;
|
||||
display: flex;
|
||||
|
|
|
@ -6,69 +6,59 @@ import type { Author } from '../../graphql/types.gen'
|
|||
import { hideModal } from '../../stores/ui'
|
||||
import { useInbox } from '../../context/inbox'
|
||||
|
||||
type InvitingUser = Author & {
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
type query =
|
||||
| {
|
||||
theme: string
|
||||
members: string[]
|
||||
}
|
||||
| undefined
|
||||
type inviteUser = Author & { selected: boolean }
|
||||
type Props = {
|
||||
users: Author[]
|
||||
}
|
||||
|
||||
const CreateModalContent = (props: Props) => {
|
||||
const inviteUsers: InvitingUser[] = props.users.map((user) => ({ ...user, selected: false }))
|
||||
const [title, setTitle] = createSignal<string>('')
|
||||
const [uids, setUids] = createSignal<number[]>([])
|
||||
const [collectionToInvite, setCollectionToInvite] = createSignal<InvitingUser[]>(inviteUsers)
|
||||
const inviteUsers: inviteUser[] = props.users.map((user) => ({ ...user, selected: false }))
|
||||
const [theme, setTheme] = createSignal<string>(' ')
|
||||
const [usersId, setUsersId] = createSignal<number[]>([])
|
||||
const [collectionToInvite, setCollectionToInvite] = createSignal<inviteUser[]>(inviteUsers)
|
||||
let textInput: HTMLInputElement
|
||||
|
||||
const reset = () => {
|
||||
setTitle('')
|
||||
setUids([])
|
||||
setTheme('')
|
||||
setUsersId([])
|
||||
hideModal()
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
console.log(collectionToInvite())
|
||||
setUids(() => {
|
||||
setUsersId(() => {
|
||||
return collectionToInvite()
|
||||
.filter((user: InvitingUser) => {
|
||||
.filter((user) => {
|
||||
return user.selected === true
|
||||
})
|
||||
.map((user: InvitingUser) => {
|
||||
return user.id
|
||||
.map((user) => {
|
||||
return user['id']
|
||||
})
|
||||
})
|
||||
if (uids().length > 1 && title().length === 0) {
|
||||
setTitle(t('group_chat'))
|
||||
if (usersId().length > 1 && theme().length === 1) {
|
||||
setTheme(t('group_chat'))
|
||||
}
|
||||
})
|
||||
|
||||
const handleSetTheme = () => {
|
||||
setTitle(textInput.value.length > 0 && textInput.value)
|
||||
setTheme(textInput.value.length > 0 && textInput.value)
|
||||
}
|
||||
|
||||
const handleClick = (user) => {
|
||||
setCollectionToInvite((userCollection: InvitingUser[]) => {
|
||||
return userCollection.map((clickedUser: InvitingUser) =>
|
||||
user.slug === clickedUser.slug ? { ...clickedUser, selected: !clickedUser.selected } : clickedUser
|
||||
setCollectionToInvite((userCollection) => {
|
||||
return userCollection.map((clickedUser) =>
|
||||
user.id === clickedUser.id ? { ...clickedUser, selected: !clickedUser.selected } : clickedUser
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const { chatEntities, actions } = useInbox()
|
||||
|
||||
console.log('!!! chatEntities:', chatEntities)
|
||||
const { actions } = useInbox()
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
const initChat = await actions.createChat(uids(), title())
|
||||
const initChat = await actions.createChat(usersId(), theme())
|
||||
console.debug('[initChat]', initChat)
|
||||
hideModal()
|
||||
await actions.loadChats()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
@ -77,7 +67,7 @@ const CreateModalContent = (props: Props) => {
|
|||
return (
|
||||
<div class={styles.CreateModalContent}>
|
||||
<h4>{t('create_chat')}</h4>
|
||||
{uids().length > 1 && (
|
||||
{usersId().length > 1 && (
|
||||
<input
|
||||
ref={textInput}
|
||||
onInput={handleSetTheme}
|
||||
|
@ -90,7 +80,7 @@ const CreateModalContent = (props: Props) => {
|
|||
|
||||
<div class="invite-recipients" style={{ height: '400px', overflow: 'auto' }}>
|
||||
<For each={collectionToInvite()}>
|
||||
{(author: InvitingUser) => (
|
||||
{(author) => (
|
||||
<InviteUser onClick={() => handleClick(author)} author={author} selected={author.selected} />
|
||||
)}
|
||||
</For>
|
||||
|
@ -104,9 +94,9 @@ const CreateModalContent = (props: Props) => {
|
|||
type="button"
|
||||
class="btn btn-lg fs-3 btn-outline-primary"
|
||||
onClick={handleCreate}
|
||||
disabled={uids().length === 0}
|
||||
disabled={usersId().length === 0}
|
||||
>
|
||||
{uids().length > 1 ? t('create_group') : t('create_chat')}
|
||||
{usersId().length > 1 ? t('create_group') : t('create_chat')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
border-radius: 50%;
|
||||
border: 3px solid #fff;
|
||||
}
|
||||
|
||||
.imageHolder {
|
||||
background-size: cover;
|
||||
width: 100%;
|
||||
|
@ -29,9 +30,6 @@
|
|||
.letter {
|
||||
display: block;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.letter {
|
||||
margin-bottom: -2px;
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
|
@ -47,4 +45,8 @@
|
|||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&.bordered {
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ type Props = {
|
|||
url?: string
|
||||
online?: boolean
|
||||
size?: 'small'
|
||||
bordered?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const colors = [
|
||||
|
@ -36,8 +38,9 @@ const DialogAvatar = (props: Props) => {
|
|||
|
||||
return (
|
||||
<div
|
||||
class={clsx(styles.DialogAvatar, {
|
||||
class={clsx(styles.DialogAvatar, props.className, {
|
||||
[styles.online]: props.online,
|
||||
[styles.bordered]: props.bordered,
|
||||
[styles.small]: props.size === 'small'
|
||||
})}
|
||||
style={{ 'background-color': `${randomBg()}` }}
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
font-size: 14px;
|
||||
padding: 12px;
|
||||
transition: background 0.3s ease-in-out;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
&.hovered:hover {
|
||||
cursor: pointer;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
|
@ -24,6 +25,7 @@
|
|||
|
||||
.name,
|
||||
.message {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
@ -67,4 +69,14 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.opened,
|
||||
&.opened:hover {
|
||||
background: #000;
|
||||
.name,
|
||||
.message,
|
||||
.time {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,36 +1,72 @@
|
|||
import styles from './DialogCard.module.scss'
|
||||
import { Show, Switch, Match, createMemo } from 'solid-js'
|
||||
import DialogAvatar from './DialogAvatar'
|
||||
import type { Author, ChatMember, User } from '../../graphql/types.gen'
|
||||
import { t } from '../../utils/intl'
|
||||
import { Show } from 'solid-js'
|
||||
import { useSession } from '../../context/session'
|
||||
import type { ChatMember } from '../../graphql/types.gen'
|
||||
import GroupDialogAvatar from './GroupDialogAvatar'
|
||||
import formattedTime from '../../utils/formatDateTime'
|
||||
import { clsx } from 'clsx'
|
||||
import styles from './DialogCard.module.scss'
|
||||
|
||||
type DialogProps = {
|
||||
online?: boolean
|
||||
message?: string
|
||||
counter?: number
|
||||
title?: string
|
||||
ownId: number
|
||||
members: ChatMember[]
|
||||
onClick?: () => void
|
||||
isChatHeader?: boolean
|
||||
lastUpdate?: number
|
||||
isOpened?: boolean
|
||||
}
|
||||
|
||||
const DialogCard = (props: DialogProps) => {
|
||||
console.log('!!! participants:', props.members)
|
||||
const companions = createMemo(
|
||||
() => props.members && props.members.filter((member) => member.id !== props.ownId)
|
||||
)
|
||||
const names = createMemo(() =>
|
||||
companions()
|
||||
?.map((companion) => companion.name)
|
||||
.join(', ')
|
||||
)
|
||||
|
||||
return (
|
||||
//DialogCardView - подумать
|
||||
<Show when={props.members?.length > 0}>
|
||||
<div class={styles.DialogCard}>
|
||||
<Show when={props.members}>
|
||||
<div
|
||||
class={clsx(styles.DialogCard, {
|
||||
[styles.header]: props.isChatHeader,
|
||||
[styles.opened]: props.isOpened,
|
||||
[styles.hovered]: !props.isChatHeader
|
||||
})}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<div class={styles.avatar}>
|
||||
<DialogAvatar name={props.members[0].name} online={props.online} />
|
||||
<Switch fallback={<DialogAvatar name={props.members[0].name} url={props.members[0].userpic} />}>
|
||||
<Match when={props.members.length >= 3}>
|
||||
<GroupDialogAvatar users={props.members} />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<div class={styles.row}>
|
||||
<div class={styles.name}>{props.members[0].name}</div>
|
||||
<div class={styles.message}>{t('You can announce your languages in profile')}</div>
|
||||
</div>
|
||||
<div class={styles.activity}>
|
||||
<div class={styles.time}>22:22</div>
|
||||
<div class={styles.counter}>
|
||||
<span>12</span>
|
||||
<div class={styles.name}>{props.title}</div>
|
||||
<div class={styles.message}>
|
||||
<Switch>
|
||||
<Match when={props.message && !props.isChatHeader}>{props.message}</Match>
|
||||
<Match when={props.isChatHeader && companions().length > 1}>{names()}</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={!props.isChatHeader}>
|
||||
<div class={styles.activity}>
|
||||
<Show when={props.lastUpdate}>
|
||||
<div class={styles.time}>{formattedTime(props.lastUpdate * 1000)}</div>
|
||||
</Show>
|
||||
<Show when={props.counter > 0}>
|
||||
<div class={styles.counter}>
|
||||
<span>{props.counter}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
|
|
9
src/components/Inbox/DialogHeader.module.scss
Normal file
9
src/components/Inbox/DialogHeader.module.scss
Normal file
|
@ -0,0 +1,9 @@
|
|||
.DialogHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 3px solid #141414;
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
}
|
||||
}
|
22
src/components/Inbox/DialogHeader.tsx
Normal file
22
src/components/Inbox/DialogHeader.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import type { Chat } from '../../graphql/types.gen'
|
||||
import styles from './DialogHeader.module.scss'
|
||||
import DialogCard from './DialogCard'
|
||||
|
||||
type DialogHeader = {
|
||||
chat: Chat
|
||||
ownId: number
|
||||
}
|
||||
const DialogHeader = (props: DialogHeader) => {
|
||||
return (
|
||||
<header class={styles.DialogHeader}>
|
||||
<DialogCard
|
||||
isChatHeader={true}
|
||||
title={props.chat.title || props.chat.members[0].name}
|
||||
members={props.chat.members}
|
||||
ownId={props.ownId}
|
||||
/>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default DialogHeader
|
47
src/components/Inbox/GroupDialogAvatar.module.scss
Normal file
47
src/components/Inbox/GroupDialogAvatar.module.scss
Normal file
|
@ -0,0 +1,47 @@
|
|||
.GroupDialogAvatar {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
|
||||
.grouped {
|
||||
position: absolute;
|
||||
|
||||
&:nth-child(1) {
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.counter {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
line-height: 21px;
|
||||
background: #fff;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: inset 0 0 0 2px #000;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
|
||||
&.hundred {
|
||||
font-size: 7px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
41
src/components/Inbox/GroupDialogAvatar.tsx
Normal file
41
src/components/Inbox/GroupDialogAvatar.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { For } from 'solid-js'
|
||||
import './DialogCard.module.scss'
|
||||
import styles from './GroupDialogAvatar.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import type { ChatMember } from '../../graphql/types.gen'
|
||||
import DialogAvatar from './DialogAvatar'
|
||||
|
||||
type Props = {
|
||||
users: ChatMember[]
|
||||
}
|
||||
|
||||
const GroupDialogAvatar = (props: Props) => {
|
||||
const slicedUsers = () => {
|
||||
if (props.users.length > 3) {
|
||||
return props.users.slice(0, 2)
|
||||
}
|
||||
return props.users.slice(0, 3)
|
||||
}
|
||||
return (
|
||||
<div class={styles.GroupDialogAvatar}>
|
||||
<For each={slicedUsers()}>
|
||||
{(user) => (
|
||||
<DialogAvatar
|
||||
className={styles.grouped}
|
||||
bordered={true}
|
||||
size="small"
|
||||
name={user.name}
|
||||
url={user.userpic}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
{props.users.length > 3 && (
|
||||
<div class={clsx(styles.counter, { [styles.hundred]: props.users.length >= 100 })}>
|
||||
{++props.users.length}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GroupDialogAvatar
|
|
@ -1,25 +1,66 @@
|
|||
$actionsWidth: 32px * 2;
|
||||
|
||||
.Message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 3.2rem 0;
|
||||
|
||||
.body {
|
||||
background: #f6f6f6;
|
||||
font-size: 14px;
|
||||
max-width: 60%;
|
||||
border-radius: 16px;
|
||||
padding: 12px 16px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
&:hover {
|
||||
.text {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
max-width: 60%;
|
||||
margin-right: auto;
|
||||
background: #f6f6f6;
|
||||
font-size: 14px;
|
||||
border-radius: 16px;
|
||||
padding: 12px 16px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
word-wrap: break-word;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: $actionsWidth;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
opacity: 0;
|
||||
right: -$actionsWidth/2;
|
||||
z-index: -1;
|
||||
transition: 0.3s ease-in-out;
|
||||
}
|
||||
&.popupVisible {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
&.popupVisible,
|
||||
&:hover {
|
||||
.actions {
|
||||
z-index: 10000;
|
||||
opacity: 1;
|
||||
right: -$actionsWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,13 +85,35 @@
|
|||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&.own {
|
||||
.body {
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
|
||||
.text {
|
||||
margin-left: auto;
|
||||
margin-right: unset;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.actions {
|
||||
right: unset;
|
||||
left: -$actionsWidth/2;
|
||||
flex-direction: row-reverse;
|
||||
.reply {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
&.popupVisible,
|
||||
&:hover {
|
||||
.actions {
|
||||
left: -$actionsWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
text-align: right;
|
||||
}
|
||||
|
|
|
@ -1,31 +1,59 @@
|
|||
import { Show } from 'solid-js'
|
||||
import { createEffect, createMemo, createSignal, Show } from 'solid-js'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { clsx } from 'clsx'
|
||||
import styles from './Message.module.scss'
|
||||
import DialogAvatar from './DialogAvatar'
|
||||
import type { Message, ChatMember } from '../../graphql/types.gen'
|
||||
import formattedTime from '../../utils/formatDateTime'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { MessageActionsPopup } from './MessageActionsPopup'
|
||||
import QuotedMessage from './QuotedMessage'
|
||||
|
||||
type Props = {
|
||||
body: string
|
||||
isOwn: boolean
|
||||
content: Message
|
||||
ownId: number
|
||||
members: ChatMember[]
|
||||
replyClick?: () => void
|
||||
replyBody?: string
|
||||
replyAuthor?: string
|
||||
}
|
||||
|
||||
const md = new MarkdownIt({
|
||||
linkify: true
|
||||
linkify: true,
|
||||
breaks: true
|
||||
})
|
||||
|
||||
const Message = (props: Props) => {
|
||||
const isOwn = props.ownId === Number(props.content.author)
|
||||
const user = props.members?.find((m) => m.id === Number(props.content.author))
|
||||
const [isPopupVisible, setIsPopupVisible] = createSignal<boolean>(false)
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.Message, props.isOwn && styles.own)}>
|
||||
<Show when={!props.isOwn}>
|
||||
<div class={clsx(styles.Message, isOwn && styles.own)}>
|
||||
<Show when={!isOwn}>
|
||||
<div class={styles.author}>
|
||||
<DialogAvatar size="small" name={'Message Author'} />
|
||||
<div class={styles.name}>Message Author</div>
|
||||
<DialogAvatar size="small" name={user.name} url={user.userpic} />
|
||||
<div class={styles.name}>{user.name}</div>
|
||||
</div>
|
||||
</Show>
|
||||
<div class={styles.body}>
|
||||
<div innerHTML={md.render(props.body)} />
|
||||
<div class={clsx(styles.body, { [styles.popupVisible]: isPopupVisible() })}>
|
||||
<div class={styles.text}>
|
||||
<div class={styles.actions}>
|
||||
<div onClick={props.replyClick}>
|
||||
<Icon name="chat-reply" class={styles.reply} />
|
||||
</div>
|
||||
<MessageActionsPopup
|
||||
onVisibilityChange={(isVisible) => setIsPopupVisible(isVisible)}
|
||||
trigger={<Icon name="menu" />}
|
||||
/>
|
||||
</div>
|
||||
<Show when={props.replyBody}>
|
||||
<QuotedMessage body={props.replyBody} variant="inline" isOwn={isOwn} />
|
||||
</Show>
|
||||
<div innerHTML={md.render(props.content.body)} />
|
||||
</div>
|
||||
</div>
|
||||
<div class={styles.time}>12:24</div>
|
||||
<div class={styles.time}>{formattedTime(props.content.createdAt * 1000)}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
43
src/components/Inbox/MessageActionsPopup.tsx
Normal file
43
src/components/Inbox/MessageActionsPopup.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { createEffect, createSignal, For } from 'solid-js'
|
||||
import type { PopupProps } from '../_shared/Popup'
|
||||
import { Popup } from '../_shared/Popup'
|
||||
import { t } from '../../utils/intl'
|
||||
|
||||
export type MessageActionType = 'reply' | 'copy' | 'pin' | 'forward' | 'select' | 'delete'
|
||||
|
||||
type MessageActionsPopup = {
|
||||
actionSelect?: (selectedAction) => void
|
||||
} & Omit<PopupProps, 'children'>
|
||||
|
||||
const actions: { name: string; action: MessageActionType }[] = [
|
||||
{ name: t('Reply'), action: 'reply' },
|
||||
{ name: t('Copy'), action: 'copy' },
|
||||
{ name: t('Pin'), action: 'pin' },
|
||||
{ name: t('Forward'), action: 'forward' },
|
||||
{ name: t('Select'), action: 'select' },
|
||||
{ name: t('Delete'), action: 'delete' }
|
||||
]
|
||||
|
||||
export const MessageActionsPopup = (props: MessageActionsPopup) => {
|
||||
const [selectedAction, setSelectedAction] = createSignal<MessageActionType | null>(null)
|
||||
|
||||
createEffect(() => {
|
||||
if (props.actionSelect) props.actionSelect(selectedAction())
|
||||
})
|
||||
return (
|
||||
<Popup {...props} variant="tiny">
|
||||
<ul class="nodash">
|
||||
<For each={actions}>
|
||||
{(item) => (
|
||||
<li
|
||||
style={item.action === 'delete' && { color: 'red' }}
|
||||
onClick={() => setSelectedAction(item.action)}
|
||||
>
|
||||
{item.name}
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</Popup>
|
||||
)
|
||||
}
|
26
src/components/Inbox/MessagesFallback.module.scss
Normal file
26
src/components/Inbox/MessagesFallback.module.scss
Normal file
|
@ -0,0 +1,26 @@
|
|||
.MessagesFallback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100%;
|
||||
|
||||
$width: 10.5em;
|
||||
|
||||
.text {
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
width: $width;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: #141414;
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
max-width: $width;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
height: 48px;
|
||||
text-transform: none;
|
||||
}
|
||||
}
|
25
src/components/Inbox/MessagesFallback.tsx
Normal file
25
src/components/Inbox/MessagesFallback.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Show } from 'solid-js'
|
||||
import styles from './MessagesFallback.module.scss'
|
||||
|
||||
type MessagesFallback = {
|
||||
message: string
|
||||
onClick?: () => void
|
||||
actionText?: string
|
||||
}
|
||||
|
||||
const MessagesFallback = (props: MessagesFallback) => {
|
||||
return (
|
||||
<div class={styles.MessagesFallback}>
|
||||
<div>
|
||||
<p class={styles.text}>{props.message}</p>
|
||||
<Show when={props.onClick}>
|
||||
<button class={styles.button} type="button" onClick={props.onClick}>
|
||||
{props.actionText}
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MessagesFallback
|
49
src/components/Inbox/QuotedMessage.module.scss
Normal file
49
src/components/Inbox/QuotedMessage.module.scss
Normal file
|
@ -0,0 +1,49 @@
|
|||
.QuotedMessage {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
|
||||
&.inline {
|
||||
color: #696969;
|
||||
border-left: 2px solid #404040;
|
||||
padding-left: 12px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&.own {
|
||||
color: #9fa1a7;
|
||||
border-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&.reply {
|
||||
border-top: 2px solid #ccc;
|
||||
padding: 12px 0;
|
||||
gap: 12px;
|
||||
|
||||
.icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-basis: 40px;
|
||||
|
||||
&.cancel {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.author {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.quote {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
43
src/components/Inbox/QuotedMessage.tsx
Normal file
43
src/components/Inbox/QuotedMessage.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { Show } from 'solid-js'
|
||||
import styles from './QuotedMessage.module.scss'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
type QuotedMessage = {
|
||||
body: string
|
||||
cancel?: () => void
|
||||
author?: string
|
||||
variant: 'inline' | 'reply'
|
||||
isOwn?: boolean
|
||||
}
|
||||
|
||||
const QuotedMessage = (props: QuotedMessage) => {
|
||||
return (
|
||||
<div
|
||||
class={clsx(styles.QuotedMessage, {
|
||||
[styles.reply]: props.variant === 'reply',
|
||||
[styles.inline]: props.variant === 'inline',
|
||||
[styles.own]: props.isOwn
|
||||
})}
|
||||
>
|
||||
<Show when={props.variant === 'reply'}>
|
||||
<div class={styles.icon}>
|
||||
<Icon name="chat-reply" />
|
||||
</div>
|
||||
</Show>
|
||||
<div class={styles.body}>
|
||||
<Show when={props.author}>
|
||||
<div class={styles.author}>{props.author}</div>
|
||||
</Show>
|
||||
<div class={styles.quote}>{props.body}</div>
|
||||
</div>
|
||||
<Show when={props.cancel && props.variant === 'reply'}>
|
||||
<div class={clsx(styles.cancel, styles.icon)} onClick={props.cancel}>
|
||||
<Icon name="close-gray" />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuotedMessage
|
|
@ -7,3 +7,7 @@ export type AuthModalSearchParams = {
|
|||
export type ConfirmEmailSearchParams = {
|
||||
token: string
|
||||
}
|
||||
|
||||
export type CreateChatSearchParams = {
|
||||
id: number
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { clsx } from 'clsx'
|
|||
import { useRouter } from '../../stores/router'
|
||||
import { t } from '../../utils/intl'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { createEffect, createSignal, Show } from 'solid-js'
|
||||
import { createSignal, Show } from 'solid-js'
|
||||
import Notifications from './Notifications'
|
||||
import { ProfilePopup } from './ProfilePopup'
|
||||
import Userpic from '../Author/Userpic'
|
||||
|
|
|
@ -17,7 +17,7 @@ interface ModalProps {
|
|||
export const Modal = (props: ModalProps) => {
|
||||
const { modal } = useModalStore()
|
||||
|
||||
const backdropClick = (event: Event) => {
|
||||
const backdropClick = () => {
|
||||
hideModal()
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ export const ProfilePopup = (props: ProfilePopupProps) => {
|
|||
} = useSession()
|
||||
|
||||
return (
|
||||
<Popup {...props} horizontalAnchor="right">
|
||||
<Popup {...props} horizontalAnchor="right" variant="bordered">
|
||||
{/*TODO: l10n*/}
|
||||
<ul class="nodash">
|
||||
<li>
|
||||
|
|
|
@ -72,7 +72,7 @@ export const ProfileSecurityPage = (props: PageProps) => {
|
|||
<h5>Google</h5>
|
||||
<div class="pretty-form__item">
|
||||
<p>
|
||||
<button class={clsx('button button--light', styles.socialButton)} type="button">
|
||||
<button class={clsx('button', 'button--light', styles.socialButton)} type="button">
|
||||
<Icon name="google" class={styles.icon} />
|
||||
Привязать
|
||||
</button>
|
||||
|
@ -82,7 +82,7 @@ export const ProfileSecurityPage = (props: PageProps) => {
|
|||
<h5>VK</h5>
|
||||
<div class="pretty-form__item">
|
||||
<p>
|
||||
<button class={clsx(styles.socialButton, 'button button--light')} type="button">
|
||||
<button class={clsx(styles.socialButton, 'button', 'button--light')} type="button">
|
||||
<Icon name="vk" class={styles.icon} />
|
||||
Привязать
|
||||
</button>
|
||||
|
@ -92,7 +92,7 @@ export const ProfileSecurityPage = (props: PageProps) => {
|
|||
<h5>Facebook</h5>
|
||||
<div class="pretty-form__item">
|
||||
<p>
|
||||
<button class={clsx(styles.socialButton, 'button button--light')} type="button">
|
||||
<button class={clsx(styles.socialButton, 'button', 'button--light')} type="button">
|
||||
<Icon name="facebook" class={styles.icon} />
|
||||
Привязать
|
||||
</button>
|
||||
|
|
|
@ -9,6 +9,7 @@ h4 {
|
|||
|
||||
h5 {
|
||||
@include font-size(1.7rem);
|
||||
|
||||
margin: 0 0 0.8rem;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { capitalize } from '../../utils'
|
||||
import styles from './Card.module.scss'
|
||||
import { createEffect, createMemo, createSignal, Show } from 'solid-js'
|
||||
import { createMemo, createSignal, Show } from 'solid-js'
|
||||
import type { Topic } from '../../graphql/types.gen'
|
||||
import { FollowingEntity } from '../../graphql/types.gen'
|
||||
import { t } from '../../utils/intl'
|
||||
|
@ -8,7 +8,6 @@ import { follow, unfollow } from '../../stores/zine/common'
|
|||
import { getLogger } from '../../utils/logger'
|
||||
import { clsx } from 'clsx'
|
||||
import { useSession } from '../../context/session'
|
||||
import { StatMetrics } from '../_shared/StatMetrics'
|
||||
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import { createEffect, createMemo, createSignal, onMount, Show, Suspense } from
|
|||
import { FullArticle } from '../Article/FullArticle'
|
||||
import { t } from '../../utils/intl'
|
||||
import type { Shout, Reaction } from '../../graphql/types.gen'
|
||||
import { useReactionsStore } from '../../stores/zine/reactions'
|
||||
|
||||
interface ArticlePageProps {
|
||||
article: Shout
|
||||
|
|
|
@ -1,51 +1,27 @@
|
|||
import { For, createSignal, Show, onMount, createEffect, createMemo } from 'solid-js'
|
||||
import type { Author, Chat, ChatMember } from '../../graphql/types.gen'
|
||||
import { AuthorCard } from '../Author/Card'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { Loading } from '../Loading'
|
||||
import type { Author, Chat, Message as MessageType } from '../../graphql/types.gen'
|
||||
import DialogCard from '../Inbox/DialogCard'
|
||||
import Search from '../Inbox/Search'
|
||||
import { useSession } from '../../context/session'
|
||||
import { createClient } from '@urql/core'
|
||||
import Message from '../Inbox/Message'
|
||||
import { loadRecipients, loadChats } from '../../stores/inbox'
|
||||
import CreateModalContent from '../Inbox/CreateModalContent'
|
||||
import DialogHeader from '../Inbox/DialogHeader'
|
||||
import MessagesFallback from '../Inbox/MessagesFallback'
|
||||
import QuotedMessage from '../Inbox/QuotedMessage'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { useSession } from '../../context/session'
|
||||
import { loadMessages, loadRecipients } from '../../stores/inbox'
|
||||
import { t } from '../../utils/intl'
|
||||
import '../../styles/Inbox.scss'
|
||||
import { useInbox } from '../../context/inbox'
|
||||
import { Modal } from '../Nav/Modal'
|
||||
import { showModal } from '../../stores/ui'
|
||||
import InviteUser from '../Inbox/InviteUser'
|
||||
import CreateModalContent from '../Inbox/CreateModalContent'
|
||||
import { useInbox } from '../../context/inbox'
|
||||
import { useRouter } from '../../stores/router'
|
||||
import { clsx } from 'clsx'
|
||||
import styles from '../../styles/Inbox.module.scss'
|
||||
|
||||
const OWNER_ID = '501'
|
||||
const client = createClient({
|
||||
url: 'https://graphqlzero.almansi.me/api'
|
||||
})
|
||||
|
||||
const messageQuery = `
|
||||
query Comments ($options: PageQueryOptions) {
|
||||
comments(options: $options) {
|
||||
data {
|
||||
id
|
||||
body
|
||||
email
|
||||
}
|
||||
}
|
||||
type InboxSearchParams = {
|
||||
initChat: string
|
||||
chat: string
|
||||
}
|
||||
`
|
||||
const newMessageQuery = `
|
||||
mutation postComment($messageBody: String!) {
|
||||
createComment(
|
||||
input: { body: $messageBody, email: "test@test.com", name: "User" }
|
||||
) {
|
||||
id
|
||||
body
|
||||
name
|
||||
email
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const userSearch = (array: Author[], keyword: string) => {
|
||||
const searchTerm = keyword.toLowerCase()
|
||||
return array.filter((value) => {
|
||||
|
@ -53,161 +29,267 @@ const userSearch = (array: Author[], keyword: string) => {
|
|||
})
|
||||
}
|
||||
|
||||
const postMessage = async (msg: string) => {
|
||||
const response = await client.mutation(newMessageQuery, { messageBody: msg }).toPromise()
|
||||
return response.data.createComment
|
||||
}
|
||||
|
||||
export const InboxView = () => {
|
||||
const [messages, setMessages] = createSignal([])
|
||||
const [recipients, setRecipients] = createSignal<Author[]>([])
|
||||
const [chats, setChats] = createSignal<Chat[]>([])
|
||||
const [cashedRecipients, setCashedRecipients] = createSignal<Author[]>([])
|
||||
const [postMessageText, setPostMessageText] = createSignal('')
|
||||
const [loading, setLoading] = createSignal<boolean>(false)
|
||||
const { session } = useSession()
|
||||
const {
|
||||
chats,
|
||||
messages,
|
||||
actions: { loadChats, getMessages, sendMessage, createChat }
|
||||
} = useInbox()
|
||||
|
||||
const [recipients, setRecipients] = createSignal<Author[]>([])
|
||||
const [postMessageText, setPostMessageText] = createSignal('')
|
||||
const [sortByGroup, setSortByGroup] = createSignal<boolean>(false)
|
||||
const [sortByPerToPer, setSortByPerToPer] = createSignal<boolean>(false)
|
||||
const [currentDialog, setCurrentDialog] = createSignal<Chat>()
|
||||
const [messageToReply, setMessageToReply] = createSignal<MessageType | null>(null)
|
||||
const { session } = useSession()
|
||||
const currentUserId = createMemo(() => session()?.user.id)
|
||||
// Поиск по диалогам
|
||||
const getQuery = (query) => {
|
||||
if (query().length >= 2) {
|
||||
const match = userSearch(recipients(), query())
|
||||
setRecipients(match)
|
||||
} else {
|
||||
setRecipients(cashedRecipients())
|
||||
}
|
||||
}
|
||||
|
||||
const fetchMessages = async (query) => {
|
||||
const response = await client
|
||||
.query(query, {
|
||||
options: { slice: { start: 0, end: 3 } }
|
||||
})
|
||||
.toPromise()
|
||||
if (response.error) console.debug('getMessages', response.error)
|
||||
setMessages(response.data.comments.data)
|
||||
// if (query().length >= 2) {
|
||||
// const match = userSearch(recipients(), query())
|
||||
// setRecipients(match)
|
||||
// } else {
|
||||
// setRecipients(cashedRecipients())
|
||||
// }
|
||||
}
|
||||
|
||||
let chatWindow
|
||||
onMount(async () => {
|
||||
setLoading(true)
|
||||
|
||||
const handleOpenChat = async (chat: Chat) => {
|
||||
setCurrentDialog(chat)
|
||||
changeSearchParam('chat', `${chat.id}`)
|
||||
try {
|
||||
await fetchMessages(messageQuery)
|
||||
await getMessages(chat.id)
|
||||
} catch (error) {
|
||||
setLoading(false)
|
||||
console.error([fetchMessages], error)
|
||||
console.error('[getMessages]', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
chatWindow.scrollTop = chatWindow.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: удалить когда будет готова подписка
|
||||
createEffect(() => {
|
||||
setInterval(async () => {
|
||||
if (!currentDialog()) return
|
||||
try {
|
||||
await getMessages(currentDialog().id)
|
||||
} catch (error) {
|
||||
console.error('[getMessages]', error)
|
||||
} finally {
|
||||
chatWindow.scrollTop = chatWindow.scrollHeight
|
||||
}
|
||||
}, 2000)
|
||||
})
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const response = await loadRecipients({ days: 365 })
|
||||
setRecipients(response as unknown as Author[])
|
||||
setCashedRecipients(response as unknown as Author[])
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await loadChats()
|
||||
setChats(response as unknown as Chat[])
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
await loadChats()
|
||||
})
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const post = await postMessage(postMessageText())
|
||||
setMessages((prev) => [...prev, post])
|
||||
setPostMessageText('')
|
||||
chatWindow.scrollTop = chatWindow.scrollHeight
|
||||
} catch (error) {
|
||||
console.error('[post message error]:', error)
|
||||
}
|
||||
await sendMessage({
|
||||
body: postMessageText().toString(),
|
||||
chat: currentDialog().id.toString(),
|
||||
replyTo: messageToReply()?.id
|
||||
})
|
||||
setPostMessageText('')
|
||||
setMessageToReply(null)
|
||||
chatWindow.scrollTop = chatWindow.scrollHeight
|
||||
}
|
||||
|
||||
let textareaParent // textarea autoresize ghost element
|
||||
const handleChangeMessage = (event) => {
|
||||
setPostMessageText(event.target.value)
|
||||
}
|
||||
createEffect(() => {
|
||||
textareaParent.dataset.replicatedValue = postMessageText()
|
||||
|
||||
const { changeSearchParam, searchParams } = useRouter<InboxSearchParams>()
|
||||
|
||||
createEffect(async () => {
|
||||
if (textareaParent) {
|
||||
textareaParent.dataset.replicatedValue = postMessageText()
|
||||
}
|
||||
if (searchParams().chat) {
|
||||
const chatToOpen = chats()?.find((chat) => chat.id === searchParams().chat)
|
||||
if (!chatToOpen) return
|
||||
await handleOpenChat(chatToOpen)
|
||||
return
|
||||
}
|
||||
if (searchParams().initChat) {
|
||||
try {
|
||||
const newChat = await createChat([Number(searchParams().initChat)], '')
|
||||
await loadChats()
|
||||
changeSearchParam('initChat', null)
|
||||
changeSearchParam('chat', newChat.chat.id)
|
||||
const chatToOpen = chats().find((chat) => chat.id === newChat.chat.id)
|
||||
await handleOpenChat(chatToOpen)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleOpenInviteModal = (event: Event) => {
|
||||
event.preventDefault()
|
||||
const handleOpenInviteModal = () => {
|
||||
showModal('inviteToChat')
|
||||
}
|
||||
|
||||
const chatsToShow = () => {
|
||||
const sorted = chats().sort((a, b) => {
|
||||
return b.updatedAt - a.updatedAt
|
||||
})
|
||||
if (sortByPerToPer()) {
|
||||
return sorted.filter((chat) => chat.title.trim().length === 0)
|
||||
} else if (sortByGroup()) {
|
||||
return sorted.filter((chat) => chat.title.trim().length > 0)
|
||||
} else {
|
||||
return sorted
|
||||
}
|
||||
}
|
||||
|
||||
const findToReply = (messageId) => {
|
||||
return messages().find((message) => message.id === messageId)
|
||||
}
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.keyCode === 13 && event.shiftKey) return
|
||||
if (event.keyCode === 13 && !event.shiftKey && postMessageText().trim().length > 0) {
|
||||
event.preventDefault()
|
||||
handleSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="messages container">
|
||||
<div class={clsx('container', styles.Inbox)}>
|
||||
<Modal variant="narrow" name="inviteToChat">
|
||||
<CreateModalContent users={recipients()} />
|
||||
</Modal>
|
||||
<div class="row">
|
||||
<div class="chat-list col-md-4">
|
||||
<div class="sidebar-header">
|
||||
<div class={clsx('row', styles.row)}>
|
||||
<div class={clsx(styles.chatList, 'col-md-4')}>
|
||||
<div class={styles.sidebarHeader}>
|
||||
<Search placeholder="Поиск" onChange={getQuery} />
|
||||
<div onClick={handleOpenInviteModal}>
|
||||
<button type="button" onClick={handleOpenInviteModal}>
|
||||
<Icon name="plus-button" style={{ width: '40px', height: '40px' }} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="chat-list__types">
|
||||
<ul>
|
||||
<li>
|
||||
<strong>{t('All')}</strong>
|
||||
</li>
|
||||
<li>{t('Personal')}</li>
|
||||
<li>{t('Groups')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="holder">
|
||||
<div class="dialogs">
|
||||
<For each={chats()}>{(chat: Chat) => <DialogCard members={chat.members} />}</For>
|
||||
<Show when={chatsToShow}>
|
||||
<div class={styles.chatListTypes}>
|
||||
<ul>
|
||||
<li
|
||||
class={clsx({ [styles.selected]: !sortByPerToPer() && !sortByGroup() })}
|
||||
onClick={() => {
|
||||
setSortByPerToPer(false)
|
||||
setSortByGroup(false)
|
||||
}}
|
||||
>
|
||||
<span>{t('All')}</span>
|
||||
</li>
|
||||
<li
|
||||
class={clsx({ [styles.selected]: sortByPerToPer() })}
|
||||
onClick={() => {
|
||||
setSortByPerToPer(true)
|
||||
setSortByGroup(false)
|
||||
}}
|
||||
>
|
||||
<span>{t('Personal')}</span>
|
||||
</li>
|
||||
<li
|
||||
class={clsx({ [styles.selected]: sortByGroup() })}
|
||||
onClick={() => {
|
||||
setSortByGroup(true)
|
||||
setSortByPerToPer(false)
|
||||
}}
|
||||
>
|
||||
<span>{t('Groups')}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Show>
|
||||
<div class={styles.holder}>
|
||||
<div class={styles.dialogs}>
|
||||
<For each={chatsToShow()}>
|
||||
{(chat) => (
|
||||
<DialogCard
|
||||
onClick={() => handleOpenChat(chat)}
|
||||
isOpened={chat.id === currentDialog()?.id}
|
||||
title={chat.title || chat.members[0].name}
|
||||
members={chat.members}
|
||||
ownId={currentUserId()}
|
||||
lastUpdate={chat.updatedAt}
|
||||
counter={chat.unread}
|
||||
message={chat.messages.pop()?.body}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 conversation">
|
||||
<div class="interlocutor user--online">
|
||||
<AuthorCard author={{} as Author} hideFollow={true} />
|
||||
<div class="user-status">Online</div>
|
||||
</div>
|
||||
|
||||
<div class="conversation__messages">
|
||||
<div class="conversation__messages-container" ref={chatWindow}>
|
||||
<Show when={loading()}>
|
||||
<Loading />
|
||||
</Show>
|
||||
<For each={messages()}>
|
||||
{(comment: { body: string; id: string; email: string }) => (
|
||||
<Message body={comment.body} isOwn={OWNER_ID === comment.id} />
|
||||
)}
|
||||
</For>
|
||||
|
||||
{/*<div class="conversation__date">*/}
|
||||
{/* <time>12 сентября</time>*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-form">
|
||||
<div class="wrapper">
|
||||
<div class="grow-wrap" ref={textareaParent}>
|
||||
<textarea
|
||||
value={postMessageText()}
|
||||
rows={1}
|
||||
onInput={(event) => handleChangeMessage(event)}
|
||||
placeholder="Написать сообщение"
|
||||
/>
|
||||
<div class={clsx('col-md-8', styles.conversation)}>
|
||||
<Show
|
||||
when={currentDialog()}
|
||||
fallback={
|
||||
<MessagesFallback
|
||||
message={t('Choose who you want to write to')}
|
||||
onClick={handleOpenInviteModal}
|
||||
actionText={t('Start conversation')}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<DialogHeader ownId={currentUserId()} chat={currentDialog()} />
|
||||
<div class={styles.conversationMessages}>
|
||||
<div class={styles.messagesContainer} ref={chatWindow}>
|
||||
<For each={messages()}>
|
||||
{(message) => (
|
||||
<Message
|
||||
content={message}
|
||||
ownId={currentUserId()}
|
||||
members={currentDialog().members}
|
||||
replyBody={message.replyTo && findToReply(message.replyTo).body}
|
||||
replyClick={() => setMessageToReply(message)}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
{/*<div class={styles.conversationDate}>*/}
|
||||
{/* <time>12 сентября</time>*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
<button type="submit" disabled={postMessageText().length === 0} onClick={handleSubmit}>
|
||||
<Icon name="send-message" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={styles.messageForm}>
|
||||
<Show when={messageToReply()}>
|
||||
<QuotedMessage
|
||||
variant="reply"
|
||||
author={
|
||||
currentDialog().members.find((member) => member.id === Number(messageToReply().author))
|
||||
.name
|
||||
}
|
||||
body={messageToReply().body}
|
||||
cancel={() => setMessageToReply(null)}
|
||||
/>
|
||||
</Show>
|
||||
<div class={styles.wrapper}>
|
||||
<div class={styles.growWrap} ref={textareaParent}>
|
||||
<textarea
|
||||
class={styles.textInput}
|
||||
value={postMessageText()}
|
||||
rows={1}
|
||||
onKeyDown={handleKeyDown}
|
||||
onInput={(event) => handleChangeMessage(event)}
|
||||
placeholder={t('Write message')}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" disabled={postMessageText().length === 0} onClick={handleSubmit}>
|
||||
<Icon name="send-message" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,9 +4,49 @@
|
|||
|
||||
.popup {
|
||||
background: #fff;
|
||||
border: 2px solid #000;
|
||||
top: calc(100% + 8px);
|
||||
opacity: 1;
|
||||
color: #000;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
min-width: 144px;
|
||||
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.bordered {
|
||||
@include font-size(1.6rem);
|
||||
|
||||
border: 2px solid #000;
|
||||
padding: 2.4rem;
|
||||
ul li {
|
||||
margin-bottom: 1.6rem;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tiny {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
box-shadow: 0 4px 60px rgba(0, 0, 0, 0.1);
|
||||
padding: 1rem;
|
||||
ul li {
|
||||
margin-bottom: 1rem;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.horizontalAnchorCenter {
|
||||
left: 50%;
|
||||
|
@ -17,25 +57,6 @@
|
|||
right: 0;
|
||||
}
|
||||
|
||||
@include font-size(1.6rem);
|
||||
|
||||
padding: 2.4rem;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 1.6rem;
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.topBorderItem {
|
||||
border-top: 2px solid;
|
||||
padding-top: 1em;
|
||||
|
|
|
@ -2,6 +2,7 @@ import { createEffect, createSignal, JSX, Show } from 'solid-js'
|
|||
import styles from './Popup.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
|
||||
import { set } from 'husky'
|
||||
|
||||
type HorizontalAnchor = 'center' | 'right'
|
||||
|
||||
|
@ -11,6 +12,7 @@ export type PopupProps = {
|
|||
children: JSX.Element
|
||||
onVisibilityChange?: (isVisible) => void
|
||||
horizontalAnchor?: HorizontalAnchor
|
||||
variant?: 'bordered' | 'tiny'
|
||||
}
|
||||
|
||||
export const Popup = (props: PopupProps) => {
|
||||
|
@ -40,7 +42,9 @@ export const Popup = (props: PopupProps) => {
|
|||
<div
|
||||
class={clsx(styles.popup, {
|
||||
[styles.horizontalAnchorCenter]: horizontalAnchor === 'center',
|
||||
[styles.horizontalAnchorRight]: horizontalAnchor === 'right'
|
||||
[styles.horizontalAnchorRight]: horizontalAnchor === 'right',
|
||||
[styles.bordered]: props.variant === 'bordered',
|
||||
[styles.tiny]: props.variant === 'tiny'
|
||||
})}
|
||||
>
|
||||
{props.children}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import styles from './SearchField.module.scss'
|
||||
import { Icon } from './Icon'
|
||||
import { t } from '../../utils/intl'
|
||||
import clsx from 'clsx'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
type SearchFieldProps = {
|
||||
onChange: (value: string) => void
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'swiper/scss'
|
|||
import 'swiper/scss/navigation'
|
||||
import 'swiper/scss/pagination'
|
||||
import './Slider.scss'
|
||||
import { createEffect, createMemo, createSignal, Show, For, JSX } from 'solid-js'
|
||||
import { createEffect, createSignal, JSX } from 'solid-js'
|
||||
import { Icon } from './Icon'
|
||||
|
||||
interface SliderProps {
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
import type { JSX } from 'solid-js'
|
||||
import { createContext, useContext } from 'solid-js'
|
||||
import type { Message } from '../graphql/types.gen'
|
||||
import { createContext, createSignal, useContext } from 'solid-js'
|
||||
import type { Accessor, JSX } from 'solid-js'
|
||||
// import { createChatClient } from '../graphql/privateGraphQLClient'
|
||||
import type { Chat, Message, MutationCreateMessageArgs } from '../graphql/types.gen'
|
||||
import { apiClient } from '../utils/apiClient'
|
||||
import { createStore } from 'solid-js/store'
|
||||
// import newMessage from '../graphql/subs/new-message'
|
||||
// import type { Client } from '@urql/core'
|
||||
import { pipe, subscribe } from 'wonka'
|
||||
import { loadMessages } from '../stores/inbox'
|
||||
|
||||
type InboxContextType = {
|
||||
chatEntities: { [chatId: string]: Message[] }
|
||||
chats: Accessor<Chat[]>
|
||||
messages?: Accessor<Message[]>
|
||||
actions: {
|
||||
createChat: (members: number[], title: string) => Promise<void>
|
||||
createChat: (members: number[], title: string) => Promise<{ chat: Chat }>
|
||||
loadChats: () => Promise<void>
|
||||
getMessages?: (chatId: string) => Promise<void>
|
||||
sendMessage?: (args: MutationCreateMessageArgs) => void
|
||||
// unsubscribe: () => void
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,20 +27,66 @@ export function useInbox() {
|
|||
}
|
||||
|
||||
export const InboxProvider = (props: { children: JSX.Element }) => {
|
||||
const [chatEntities, setChatEntities] = createStore({})
|
||||
const [chats, setChats] = createSignal<Chat[]>([])
|
||||
const [messages, setMessages] = createSignal<Message[]>([])
|
||||
// const subclient = createMemo<Client>(() => createChatClient())
|
||||
const loadChats = async () => {
|
||||
try {
|
||||
const newChats = await apiClient.getChats({ limit: 50, offset: 0 })
|
||||
setChats(newChats)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
const getMessages = async (chatId: string) => {
|
||||
if (!chatId) return
|
||||
try {
|
||||
const response = await loadMessages({ chat: chatId })
|
||||
setMessages(response as unknown as Message[])
|
||||
} catch (error) {
|
||||
console.error('[loadMessages]', error)
|
||||
}
|
||||
}
|
||||
|
||||
const sendMessage = async (args) => {
|
||||
try {
|
||||
const message = await apiClient.createMessage(args)
|
||||
setMessages((prev) => [...prev, message])
|
||||
const currentChat = chats().find((chat) => chat.id === args.chat)
|
||||
setChats((prev) => [
|
||||
...prev.filter((c) => c.id !== currentChat.id),
|
||||
{ ...currentChat, updatedAt: message.createdAt }
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('[post message error]:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const createChat = async (members: number[], title: string) => {
|
||||
const chat = await apiClient.createChat({ members, title })
|
||||
setChatEntities((s) => {
|
||||
s[chat.id] = chat
|
||||
setChats((prevChats) => {
|
||||
return [chat, ...prevChats]
|
||||
})
|
||||
return chat
|
||||
}
|
||||
|
||||
const { unsubscribe } = pipe(
|
||||
() => null, // subclient().subscription(newMessage, {}),
|
||||
subscribe((result) => {
|
||||
console.info('[subscription]')
|
||||
console.debug(result)
|
||||
// TODO: handle data result
|
||||
})
|
||||
)
|
||||
const actions = {
|
||||
createChat
|
||||
createChat,
|
||||
loadChats,
|
||||
getMessages,
|
||||
sendMessage,
|
||||
unsubscribe // TODO: call unsubscribe some time!
|
||||
}
|
||||
|
||||
const value: InboxContextType = { chatEntities, actions }
|
||||
const value: InboxContextType = { chats, messages, actions }
|
||||
return <InboxContext.Provider value={value}>{props.children}</InboxContext.Provider>
|
||||
}
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import { gql } from '@urql/core'
|
||||
|
||||
export default gql`
|
||||
mutation createMessage($chat: String!, $body: String!) {
|
||||
createMessage(chat: $chat, body: $body) {
|
||||
mutation createMessage($chat: String!, $body: String!, $replyTo: Int) {
|
||||
createMessage(chat: $chat, body: $body, replyTo: $replyTo) {
|
||||
error
|
||||
author {
|
||||
slug
|
||||
message {
|
||||
id
|
||||
body
|
||||
author
|
||||
createdAt
|
||||
replyTo
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ export default gql`
|
|||
token
|
||||
user {
|
||||
_id: slug
|
||||
id
|
||||
name
|
||||
slug
|
||||
bio
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
import { ClientOptions, dedupExchange, fetchExchange, Exchange, createClient } from '@urql/core'
|
||||
import {
|
||||
ClientOptions,
|
||||
dedupExchange,
|
||||
fetchExchange,
|
||||
Exchange,
|
||||
subscriptionExchange,
|
||||
createClient
|
||||
} from '@urql/core'
|
||||
import { createClient as createSubClient } from 'graphql-sse'
|
||||
// import { createClient as createSubClient } from 'graphql-ws'
|
||||
import { devtoolsExchange } from '@urql/devtools'
|
||||
import { isDev, apiBaseUrl } from '../utils/config'
|
||||
// import { cache } from './cache'
|
||||
|
@ -28,7 +37,7 @@ export const resetToken = () => {
|
|||
}
|
||||
|
||||
const options: ClientOptions = {
|
||||
url: apiBaseUrl,
|
||||
url: apiBaseUrl + '/graphql',
|
||||
maskTypename: true,
|
||||
requestPolicy: 'cache-and-network',
|
||||
fetchOptions: () => {
|
||||
|
@ -45,3 +54,25 @@ const options: ClientOptions = {
|
|||
}
|
||||
|
||||
export const privateGraphQLClient = createClient(options)
|
||||
|
||||
export const createChatClient = () => {
|
||||
const subClient = createSubClient({
|
||||
url: apiBaseUrl + '/messages' // .replace('http', 'ws')
|
||||
})
|
||||
|
||||
const subExchange = subscriptionExchange({
|
||||
forwardSubscription(operation) {
|
||||
return {
|
||||
subscribe: (sink) => {
|
||||
const dispose = subClient.subscribe(operation, sink)
|
||||
return {
|
||||
unsubscribe: dispose
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
options.exchanges.unshift(subExchange)
|
||||
return createClient(options)
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ if (isDev) {
|
|||
}
|
||||
|
||||
const options: ClientOptions = {
|
||||
url: apiBaseUrl,
|
||||
url: apiBaseUrl + '/graphql',
|
||||
maskTypename: true,
|
||||
requestPolicy: 'cache-and-network',
|
||||
exchanges
|
||||
|
|
|
@ -4,6 +4,7 @@ export default gql`
|
|||
query GetAuthorBySlugQuery($slug: String!) {
|
||||
getAuthor(slug: $slug) {
|
||||
_id: slug
|
||||
id
|
||||
slug
|
||||
name
|
||||
bio
|
||||
|
|
|
@ -4,6 +4,7 @@ export default gql`
|
|||
query UserSubscribersQuery($slug: String!) {
|
||||
userSubcribers(slug: $slug) {
|
||||
_id: slug
|
||||
id
|
||||
slug
|
||||
name
|
||||
bio
|
||||
|
|
|
@ -4,6 +4,7 @@ export default gql`
|
|||
query UserFollowingQuery($slug: String!) {
|
||||
userFollowing(slug: $slug) {
|
||||
_id: slug
|
||||
id
|
||||
slug
|
||||
name
|
||||
bio
|
||||
|
|
|
@ -4,6 +4,7 @@ export default gql`
|
|||
query AuthorsAllQuery {
|
||||
authorsAll {
|
||||
_id: slug
|
||||
id
|
||||
slug
|
||||
name
|
||||
bio
|
||||
|
|
|
@ -4,6 +4,7 @@ export default gql`
|
|||
query AuthorLoadByQuery($by: AuthorsBy, $limit: Int, $offset: Int) {
|
||||
loadAuthorsBy(by: $by, limit: $limit, offset: $offset) {
|
||||
_id: slug
|
||||
id
|
||||
slug
|
||||
name
|
||||
bio
|
||||
|
|
|
@ -7,9 +7,11 @@ export default gql`
|
|||
messages {
|
||||
author
|
||||
body
|
||||
replyTo
|
||||
createdAt
|
||||
id
|
||||
updatedAt
|
||||
seen
|
||||
replyTo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ export default gql`
|
|||
members {
|
||||
id
|
||||
name
|
||||
id
|
||||
slug
|
||||
userpic
|
||||
}
|
||||
|
|
|
@ -6,11 +6,19 @@ export default gql`
|
|||
error
|
||||
chats {
|
||||
id
|
||||
title
|
||||
admins
|
||||
users
|
||||
members {
|
||||
id
|
||||
slug
|
||||
name
|
||||
userpic
|
||||
}
|
||||
unread
|
||||
description
|
||||
updatedAt
|
||||
private
|
||||
messages {
|
||||
id
|
||||
body
|
||||
|
|
15
src/graphql/subs/new-message.ts
Normal file
15
src/graphql/subs/new-message.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { gql } from '@urql/core'
|
||||
|
||||
export default gql`
|
||||
subscription {
|
||||
newMessage {
|
||||
id
|
||||
chatId
|
||||
author
|
||||
body
|
||||
replyTo
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
|
@ -146,7 +146,7 @@ export type Message = {
|
|||
chatId: Scalars['String']
|
||||
createdAt: Scalars['Int']
|
||||
id: Scalars['Int']
|
||||
replyTo?: Maybe<Scalars['String']>
|
||||
replyTo?: Maybe<Scalars['Int']>
|
||||
seen?: Maybe<Scalars['Boolean']>
|
||||
updatedAt?: Maybe<Scalars['Int']>
|
||||
}
|
||||
|
|
|
@ -184,12 +184,13 @@
|
|||
"discussion": "дискурс",
|
||||
"Personal": "Личные",
|
||||
"Groups": "Группы",
|
||||
"All": "Все",
|
||||
"create_chat": "Создать чат",
|
||||
"create_group": "Создать группу",
|
||||
"discourse_theme": "Тема дискурса",
|
||||
"cancel": "Отмена",
|
||||
"group_chat": "Общий чат",
|
||||
"Choose who you want to write to": "Выберите кому хотите написать",
|
||||
"Start conversation": "Начать беседу",
|
||||
"Profile settings": "Настройки профиля",
|
||||
"Here you can customize your profile the way you want.": "Здесь можно настроить свой профиль так, как вы хотите.",
|
||||
"Userpic": "Аватар",
|
||||
|
@ -204,6 +205,11 @@
|
|||
"Date of Birth": "Дата рождения",
|
||||
"Social networks": "Социальные сети",
|
||||
"Save settings": "Сохранить настройки",
|
||||
"Write message": "Написать сообщение",
|
||||
"Copy": "Скопировать",
|
||||
"Pin": "Закрепить",
|
||||
"Forward": "Переслать",
|
||||
"Select": "Выбрать",
|
||||
"slug is used by another user": "Имя уже занято другим пользователем",
|
||||
"It does not look like url": "Это не похоже на ссылку"
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { apiClient } from '../utils/apiClient'
|
||||
import type { MessagesBy } from '../graphql/types.gen'
|
||||
|
||||
export const loadRecipients = async (by = {}): Promise<void> => {
|
||||
return await apiClient.getRecipients(by)
|
||||
}
|
||||
|
||||
export const loadChats = async (): Promise<void> => {
|
||||
return await apiClient.getChats({ limit: 50, offset: 0 })
|
||||
export const loadMessages = async (by: MessagesBy): Promise<void> => {
|
||||
return await apiClient.getChatMessages({ by, limit: 50, offset: 0 })
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import type { FollowingEntity } from '../../graphql/types.gen'
|
|||
import { apiClient } from '../../utils/apiClient'
|
||||
|
||||
export const follow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
|
||||
console.log('!!! follow:')
|
||||
await apiClient.follow({ what, slug })
|
||||
}
|
||||
export const unfollow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
|
||||
|
|
|
@ -60,6 +60,7 @@ img {
|
|||
|
||||
.shoutMediaBody {
|
||||
display: block;
|
||||
|
||||
audio {
|
||||
display: inline-block;
|
||||
}
|
||||
|
@ -258,6 +259,7 @@ img {
|
|||
|
||||
.commentsHeader {
|
||||
@include font-size(2.4rem);
|
||||
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
|
@ -287,6 +289,7 @@ img {
|
|||
|
||||
button {
|
||||
@include font-size(1.5rem);
|
||||
|
||||
border-radius: 0.8rem;
|
||||
margin-right: 1.2rem;
|
||||
padding: 1.1rem 1.2rem 0.9rem;
|
||||
|
|
|
@ -5,17 +5,18 @@ main {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.messages {
|
||||
display: none;
|
||||
top: 74px;
|
||||
.Inbox {
|
||||
top: 84px;
|
||||
height: calc(100% - 74px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding-left: 42px;
|
||||
padding-right: 26px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
|
||||
.row {
|
||||
flex: 1;
|
||||
|
@ -31,7 +32,7 @@ main {
|
|||
}
|
||||
|
||||
// список диалогов и юзеров
|
||||
.chat-list {
|
||||
.chatList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
|
@ -39,7 +40,7 @@ main {
|
|||
|
||||
$fade-height: 10px;
|
||||
|
||||
.sidebar-header {
|
||||
.sidebarHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
@ -56,7 +57,7 @@ main {
|
|||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
right: 10px;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
height: $fade-height;
|
||||
}
|
||||
|
@ -87,14 +88,13 @@ main {
|
|||
}
|
||||
}
|
||||
|
||||
.chat-list__search,
|
||||
.interlocutor {
|
||||
.chat-list__search {
|
||||
border-bottom: 3px solid #141414;
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
// табы выбора списка
|
||||
.chat-list__types {
|
||||
.chatListTypes {
|
||||
@include font-size(1.7rem);
|
||||
|
||||
margin: 16px 0;
|
||||
|
@ -110,51 +110,16 @@ main {
|
|||
li {
|
||||
margin-right: 1em;
|
||||
color: #696969;
|
||||
}
|
||||
cursor: pointer;
|
||||
|
||||
strong {
|
||||
border-bottom: 3px solid;
|
||||
font-weight: normal;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.interlocutor {
|
||||
height: 56px;
|
||||
box-sizing: content-box;
|
||||
|
||||
.circlewrap {
|
||||
height: 56px;
|
||||
max-width: 56px;
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
.author {
|
||||
margin-bottom: 0;
|
||||
|
||||
&::before {
|
||||
left: 40px !important;
|
||||
height: 8px !important;
|
||||
width: 8px !important;
|
||||
&.selected {
|
||||
span {
|
||||
border-bottom: 3px solid;
|
||||
font-weight: normal;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.author__name {
|
||||
@include font-size(1.7rem);
|
||||
|
||||
margin: 0.4em 0 0;
|
||||
}
|
||||
|
||||
.author__details,
|
||||
.user-status {
|
||||
margin-left: 6.8rem;
|
||||
}
|
||||
|
||||
.user-status {
|
||||
@include font-size(1.2rem);
|
||||
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation {
|
||||
|
@ -162,36 +127,31 @@ main {
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.conversation__messages {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-form {
|
||||
.messageForm {
|
||||
background: #fff;
|
||||
padding: 2px 0 12px 0;
|
||||
padding: 2px 0 12px;
|
||||
|
||||
.wrapper {
|
||||
border: 2px solid #cccccc;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 16px;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.grow-wrap {
|
||||
.growWrap {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
|
||||
&::after {
|
||||
content: attr(data-replicated-value) ' ';
|
||||
content: attr(data-replicated-value);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
visibility: hidden;
|
||||
transition: height 1.3s ease-in-out;
|
||||
}
|
||||
|
||||
& textarea {
|
||||
.textInput {
|
||||
margin-bottom: 0;
|
||||
font-family: inherit;
|
||||
border: none;
|
||||
|
@ -243,36 +203,40 @@ main {
|
|||
}
|
||||
}
|
||||
|
||||
.conversation__messages-container {
|
||||
left: 0;
|
||||
height: 100%;
|
||||
.conversationMessages {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.conversation__date {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
&::before {
|
||||
background: #141414;
|
||||
content: '';
|
||||
height: 1px;
|
||||
.messagesContainer {
|
||||
left: 0;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
top: 0.8em;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: -1;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
.conversation__date {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
time {
|
||||
background: #fff;
|
||||
@include font-size(1.5rem);
|
||||
&::before {
|
||||
background: #141414;
|
||||
content: '';
|
||||
height: 1px;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0.8em;
|
||||
width: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
color: #9fa1a7;
|
||||
padding: 0 0.5em;
|
||||
time {
|
||||
@include font-size(1.5rem);
|
||||
|
||||
background: #fff;
|
||||
color: #9fa1a7;
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -85,8 +85,8 @@ h2 {
|
|||
color: #fff;
|
||||
margin-left: -0.15em;
|
||||
padding: 0 0.15em;
|
||||
box-decoration-break: clone;
|
||||
-webkit-box-decoration-break: clone;
|
||||
box-decoration-break: clone;
|
||||
|
||||
&::selection {
|
||||
background: #fff;
|
||||
|
@ -809,5 +809,6 @@ details {
|
|||
|
||||
.description {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
color: rgba(0 0 0 / 40%);
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ import type {
|
|||
QueryLoadMessagesByArgs,
|
||||
MutationCreateChatArgs,
|
||||
MutationCreateMessageArgs,
|
||||
Chat,
|
||||
QueryLoadRecipientsArgs,
|
||||
User,
|
||||
ProfileInput
|
||||
} from '../graphql/types.gen'
|
||||
import { publicGraphQLClient } from '../graphql/publicGraphQLClient'
|
||||
|
@ -276,7 +276,7 @@ export const apiClient = {
|
|||
},
|
||||
|
||||
// inbox
|
||||
getChats: async (options: QueryLoadChatsArgs) => {
|
||||
getChats: async (options: QueryLoadChatsArgs): Promise<Chat[]> => {
|
||||
const resp = await privateGraphQLClient.query(myChats, options).toPromise()
|
||||
return resp.data.loadChats.chats
|
||||
},
|
||||
|
@ -288,13 +288,15 @@ export const apiClient = {
|
|||
|
||||
createMessage: async (options: MutationCreateMessageArgs) => {
|
||||
const resp = await privateGraphQLClient.mutation(createMessage, options).toPromise()
|
||||
return resp.data.createMessage
|
||||
return resp.data.createMessage.message
|
||||
},
|
||||
|
||||
getChatMessages: async (options: QueryLoadMessagesByArgs) => {
|
||||
const resp = await privateGraphQLClient.query(chatMessagesLoadBy, options).toPromise()
|
||||
return resp.data.loadChat
|
||||
console.log('[getChatMessages]', resp)
|
||||
return resp.data.loadMessagesBy.messages
|
||||
},
|
||||
|
||||
getRecipients: async (options: QueryLoadRecipientsArgs) => {
|
||||
const resp = await privateGraphQLClient.query(loadRecipients, options).toPromise()
|
||||
return resp.data.loadRecipients.members
|
||||
|
|
13
src/utils/formatDateTime.ts
Normal file
13
src/utils/formatDateTime.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { createMemo } from 'solid-js'
|
||||
import { locale } from '../stores/ui'
|
||||
|
||||
// unix timestamp in seconds
|
||||
const formattedTime = (time: number) =>
|
||||
createMemo<string>(() => {
|
||||
return new Date(time).toLocaleTimeString(locale(), {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
})
|
||||
})
|
||||
|
||||
export default formattedTime
|
|
@ -4,7 +4,7 @@ export const groupByName = (arr: Author[]) => {
|
|||
return arr.reduce(
|
||||
(acc, tt) => {
|
||||
let c = (tt.name || '')
|
||||
.replace(/[^\d A-Za-zА-я]/g, '')
|
||||
.replaceAll(/[^\d A-Za-zА-я]/g, '')
|
||||
.split(' ')
|
||||
.pop()
|
||||
.slice(0, 1)
|
||||
|
@ -24,7 +24,7 @@ export const groupByTitle = (arr: (Shout | Topic)[]) => {
|
|||
return arr.reduce(
|
||||
(acc, tt) => {
|
||||
let c = (tt.title || '')
|
||||
.replace(/[^\d A-Za-zА-я]/g, '')
|
||||
.replaceAll(/[^\d A-Za-zА-я]/g, '')
|
||||
.slice(0, 1)
|
||||
.toUpperCase()
|
||||
if (/[^А-я]/.test(c)) c = 'A-Z'
|
||||
|
|
53
yarn.lock
53
yarn.lock
|
@ -4407,11 +4407,6 @@ comma-separated-tokens@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
|
||||
integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
|
||||
|
||||
commander@^8.0.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
|
||||
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
|
||||
|
||||
commander@^9.3.0:
|
||||
version "9.4.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.1.tgz#d1dd8f2ce6faf93147295c0df13c7c21141cfbdd"
|
||||
|
@ -5119,7 +5114,7 @@ esbuild-windows-arm64@0.15.15:
|
|||
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.15.tgz#5a277ce10de999d2a6465fc92a8c2a2d207ebd31"
|
||||
integrity sha512-ttuoCYCIJAFx4UUKKWYnFdrVpoXa3+3WWkXVI6s09U+YjhnyM5h96ewTq/WgQj9LFSIlABQvadHSOQyAVjW5xQ==
|
||||
|
||||
esbuild@^0.14.0, esbuild@^0.14.27, esbuild@^0.14.43:
|
||||
esbuild@^0.14.0, esbuild@^0.14.43:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2"
|
||||
integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==
|
||||
|
@ -5949,7 +5944,7 @@ glob-parent@^6.0.2:
|
|||
dependencies:
|
||||
is-glob "^4.0.3"
|
||||
|
||||
glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.7:
|
||||
glob@^7.1.1, glob@^7.1.3, glob@^7.1.4:
|
||||
version "7.2.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
||||
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
|
||||
|
@ -6085,6 +6080,11 @@ graphql-request@^5.0.0:
|
|||
extract-files "^9.0.0"
|
||||
form-data "^3.0.0"
|
||||
|
||||
graphql-sse@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql-sse/-/graphql-sse-1.3.1.tgz#74304c6754702431a62f576a67969bc2ca5c7d7f"
|
||||
integrity sha512-JIeBJsk1kGQQjfrDu0KWy5UBMvDHwcHYBmKb+4ZPZHaws/j00H7fw5CH5Vb077D7LCta+KNQA0xcGGPmIHzu4A==
|
||||
|
||||
graphql-tag@^2.11.0, graphql-tag@^2.12.6:
|
||||
version "2.12.6"
|
||||
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1"
|
||||
|
@ -8977,7 +8977,7 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
|||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@^8.3.11, postcss@^8.3.5, postcss@^8.4.13, postcss@^8.4.14, postcss@^8.4.18, postcss@^8.4.19:
|
||||
postcss@^8.3.11, postcss@^8.4.14, postcss@^8.4.18, postcss@^8.4.19:
|
||||
version "8.4.19"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.19.tgz#61178e2add236b17351897c8bcc0b4c8ecab56fc"
|
||||
integrity sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==
|
||||
|
@ -9215,16 +9215,6 @@ punycode@^2.1.0:
|
|||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||
|
||||
purgecss@^4.1.1:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-4.1.3.tgz#683f6a133c8c4de7aa82fe2746d1393b214918f7"
|
||||
integrity sha512-99cKy4s+VZoXnPxaoM23e5ABcP851nC2y2GROkkjS8eJaJtlciGavd7iYAw2V84WeBqggZ12l8ef44G99HmTaw==
|
||||
dependencies:
|
||||
commander "^8.0.0"
|
||||
glob "^7.1.7"
|
||||
postcss "^8.3.5"
|
||||
postcss-selector-parser "^6.0.6"
|
||||
|
||||
pvtsutils@^1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.2.tgz#9f8570d132cdd3c27ab7d51a2799239bf8d8d5de"
|
||||
|
@ -9624,13 +9614,6 @@ rollup-pluginutils@^2.8.2:
|
|||
dependencies:
|
||||
estree-walker "^0.6.1"
|
||||
|
||||
"rollup@>=2.59.0 <2.78.0":
|
||||
version "2.77.3"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.77.3.tgz#8f00418d3a2740036e15deb653bed1a90ee0cc12"
|
||||
integrity sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
rollup@^2.79.1:
|
||||
version "2.79.1"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7"
|
||||
|
@ -10910,26 +10893,6 @@ vfile@^5.0.0, vfile@^5.3.2:
|
|||
unist-util-stringify-position "^3.0.0"
|
||||
vfile-message "^3.0.0"
|
||||
|
||||
vite-plugin-html-purgecss@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/vite-plugin-html-purgecss/-/vite-plugin-html-purgecss-0.1.1.tgz#e392c4c26470c1a80d45e70c5638cd07866e1292"
|
||||
integrity sha512-/VJnN/CkUoXlgVCvIbFymfsW7hUEO2Dch5uWwiKJFTb4SLLNhTr/sPJfEUl1wTj5y3SwPXgPz002sQgXJj0mCw==
|
||||
dependencies:
|
||||
purgecss "^4.1.1"
|
||||
vite "^2.6.7"
|
||||
|
||||
vite@^2.6.7:
|
||||
version "2.9.15"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.15.tgz#2858dd5b2be26aa394a283e62324281892546f0b"
|
||||
integrity sha512-fzMt2jK4vQ3yK56te3Kqpkaeq9DkcZfBbzHwYpobasvgYmP2SoAr6Aic05CsB4CzCZbsDv4sujX3pkEGhLabVQ==
|
||||
dependencies:
|
||||
esbuild "^0.14.27"
|
||||
postcss "^8.4.13"
|
||||
resolve "^1.22.0"
|
||||
rollup ">=2.59.0 <2.78.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
vite@^3.2.4, vite@~3.2.4:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.4.tgz#d8c7892dd4268064e04fffbe7d866207dd24166e"
|
||||
|
|
Loading…
Reference in New Issue
Block a user