parent
2e74624240
commit
0b70289195
|
@ -12,7 +12,7 @@ const getDevCssClassPrefix = (filename: string): string => {
|
||||||
return filename
|
return filename
|
||||||
.slice(filename.indexOf(PATH_PREFIX) + PATH_PREFIX.length)
|
.slice(filename.indexOf(PATH_PREFIX) + PATH_PREFIX.length)
|
||||||
.replace('.module.scss', '')
|
.replace('.module.scss', '')
|
||||||
.replace(/[/?\\]/g, '-')
|
.replaceAll(/[/?\\]/g, '-')
|
||||||
}
|
}
|
||||||
|
|
||||||
const devGenerateScopedName = (name: string, filename: string, css: string) =>
|
const devGenerateScopedName = (name: string, filename: string, css: string) =>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
overwrite: true
|
overwrite: true
|
||||||
schema: 'http://localhost:8080'
|
#schema: 'http://localhost:8080'
|
||||||
|
schema: 'https://v2.discours.io'
|
||||||
generates:
|
generates:
|
||||||
src/graphql/introspec.gen.ts:
|
src/graphql/introspec.gen.ts:
|
||||||
plugins:
|
plugins:
|
||||||
|
|
|
@ -84,6 +84,7 @@
|
||||||
"eslint-plugin-sonarjs": "^0.16.0",
|
"eslint-plugin-sonarjs": "^0.16.0",
|
||||||
"eslint-plugin-unicorn": "^45.0.0",
|
"eslint-plugin-unicorn": "^45.0.0",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
|
"graphql-sse": "^1.3.1",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"graphql-ws": "^5.11.2",
|
"graphql-ws": "^5.11.2",
|
||||||
"hast-util-select": "^5.0.2",
|
"hast-util-select": "^5.0.2",
|
||||||
|
@ -140,7 +141,6 @@
|
||||||
"unique-names-generator": "^4.7.1",
|
"unique-names-generator": "^4.7.1",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"vite": "^3.2.4",
|
"vite": "^3.2.4",
|
||||||
"vite-plugin-html-purgecss": "^0.1.1",
|
|
||||||
"ws": "^8.11.0",
|
"ws": "^8.11.0",
|
||||||
"y-prosemirror": "^1.2.0",
|
"y-prosemirror": "^1.2.0",
|
||||||
"y-protocols": "^1.0.5",
|
"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 {
|
.commentControls {
|
||||||
@include font-size(1.2rem);
|
@include font-size(1.2rem);
|
||||||
|
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,6 +121,7 @@
|
||||||
|
|
||||||
.commentBody {
|
.commentBody {
|
||||||
@include font-size(1.5rem);
|
@include font-size(1.5rem);
|
||||||
|
|
||||||
line-height: 1.47;
|
line-height: 1.47;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,6 +198,7 @@
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@include font-size(1.6rem);
|
@include font-size(1.6rem);
|
||||||
|
|
||||||
margin-left: 1.2rem;
|
margin-left: 1.2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import styles from './RatingControl.module.scss'
|
import styles from './RatingControl.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Icon } from '../_shared/Icon'
|
|
||||||
|
|
||||||
interface RatingControlProps {
|
interface RatingControlProps {
|
||||||
rating?: number
|
rating?: number
|
||||||
|
|
|
@ -9,7 +9,7 @@ type SharePopupProps = Omit<PopupProps, 'children'>
|
||||||
|
|
||||||
export const SharePopup = (props: SharePopupProps) => {
|
export const SharePopup = (props: SharePopupProps) => {
|
||||||
return (
|
return (
|
||||||
<Popup {...props}>
|
<Popup {...props} variant="bordered">
|
||||||
<ul class="nodash">
|
<ul class="nodash">
|
||||||
<li>
|
<li>
|
||||||
<a href="#">
|
<a href="#">
|
||||||
|
|
|
@ -265,6 +265,7 @@
|
||||||
.authorComments {
|
.authorComments {
|
||||||
.authorName {
|
.authorName {
|
||||||
@include font-size(1.2rem);
|
@include font-size(1.2rem);
|
||||||
|
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,10 @@ import { follow, unfollow } from '../../stores/zine/common'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import { StatMetrics } from '../_shared/StatMetrics'
|
import { StatMetrics } from '../_shared/StatMetrics'
|
||||||
import { FollowingEntity } from '../../graphql/types.gen'
|
|
||||||
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
||||||
|
import { FollowingEntity } from '../../graphql/types.gen'
|
||||||
|
import { router, useRouter } from '../../stores/router'
|
||||||
|
import { openPage } from '@nanostores/router'
|
||||||
|
|
||||||
interface AuthorCardProps {
|
interface AuthorCardProps {
|
||||||
caption?: string
|
caption?: string
|
||||||
|
@ -68,6 +70,11 @@ export const AuthorCard = (props: AuthorCardProps) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: reimplement AuthorCard
|
// TODO: reimplement AuthorCard
|
||||||
|
const { changeSearchParam } = useRouter()
|
||||||
|
const initChat = () => {
|
||||||
|
openPage(router, `inbox`)
|
||||||
|
changeSearchParam('initChat', `${props.author.id}`)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={clsx(styles.author)}
|
class={clsx(styles.author)}
|
||||||
|
@ -163,6 +170,7 @@ export const AuthorCard = (props: AuthorCardProps) => {
|
||||||
'button--subscribe-topic': props.isAuthorsList,
|
'button--subscribe-topic': props.isAuthorsList,
|
||||||
[styles.buttonWrite]: props.liteButtons && props.isAuthorsList
|
[styles.buttonWrite]: props.liteButtons && props.isAuthorsList
|
||||||
}}
|
}}
|
||||||
|
onClick={initChat}
|
||||||
>
|
>
|
||||||
<Icon name="comment" class={styles.icon} />
|
<Icon name="comment" class={styles.icon} />
|
||||||
<Show when={!props.liteButtons}>{t('Write')}</Show>
|
<Show when={!props.liteButtons}>{t('Write')}</Show>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
.CreateModalContent {
|
.CreateModalContent {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -6,69 +6,59 @@ import type { Author } from '../../graphql/types.gen'
|
||||||
import { hideModal } from '../../stores/ui'
|
import { hideModal } from '../../stores/ui'
|
||||||
import { useInbox } from '../../context/inbox'
|
import { useInbox } from '../../context/inbox'
|
||||||
|
|
||||||
type InvitingUser = Author & {
|
type inviteUser = Author & { selected: boolean }
|
||||||
selected: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type query =
|
|
||||||
| {
|
|
||||||
theme: string
|
|
||||||
members: string[]
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
type Props = {
|
type Props = {
|
||||||
users: Author[]
|
users: Author[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateModalContent = (props: Props) => {
|
const CreateModalContent = (props: Props) => {
|
||||||
const inviteUsers: InvitingUser[] = props.users.map((user) => ({ ...user, selected: false }))
|
const inviteUsers: inviteUser[] = props.users.map((user) => ({ ...user, selected: false }))
|
||||||
const [title, setTitle] = createSignal<string>('')
|
const [theme, setTheme] = createSignal<string>(' ')
|
||||||
const [uids, setUids] = createSignal<number[]>([])
|
const [usersId, setUsersId] = createSignal<number[]>([])
|
||||||
const [collectionToInvite, setCollectionToInvite] = createSignal<InvitingUser[]>(inviteUsers)
|
const [collectionToInvite, setCollectionToInvite] = createSignal<inviteUser[]>(inviteUsers)
|
||||||
let textInput: HTMLInputElement
|
let textInput: HTMLInputElement
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
setTitle('')
|
setTheme('')
|
||||||
setUids([])
|
setUsersId([])
|
||||||
hideModal()
|
hideModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
console.log(collectionToInvite())
|
setUsersId(() => {
|
||||||
setUids(() => {
|
|
||||||
return collectionToInvite()
|
return collectionToInvite()
|
||||||
.filter((user: InvitingUser) => {
|
.filter((user) => {
|
||||||
return user.selected === true
|
return user.selected === true
|
||||||
})
|
})
|
||||||
.map((user: InvitingUser) => {
|
.map((user) => {
|
||||||
return user.id
|
return user['id']
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
if (uids().length > 1 && title().length === 0) {
|
if (usersId().length > 1 && theme().length === 1) {
|
||||||
setTitle(t('group_chat'))
|
setTheme(t('group_chat'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSetTheme = () => {
|
const handleSetTheme = () => {
|
||||||
setTitle(textInput.value.length > 0 && textInput.value)
|
setTheme(textInput.value.length > 0 && textInput.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClick = (user) => {
|
const handleClick = (user) => {
|
||||||
setCollectionToInvite((userCollection: InvitingUser[]) => {
|
setCollectionToInvite((userCollection) => {
|
||||||
return userCollection.map((clickedUser: InvitingUser) =>
|
return userCollection.map((clickedUser) =>
|
||||||
user.slug === clickedUser.slug ? { ...clickedUser, selected: !clickedUser.selected } : clickedUser
|
user.id === clickedUser.id ? { ...clickedUser, selected: !clickedUser.selected } : clickedUser
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const { chatEntities, actions } = useInbox()
|
const { actions } = useInbox()
|
||||||
|
|
||||||
console.log('!!! chatEntities:', chatEntities)
|
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
try {
|
try {
|
||||||
const initChat = await actions.createChat(uids(), title())
|
const initChat = await actions.createChat(usersId(), theme())
|
||||||
console.debug('[initChat]', initChat)
|
console.debug('[initChat]', initChat)
|
||||||
|
hideModal()
|
||||||
|
await actions.loadChats()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
@ -77,7 +67,7 @@ const CreateModalContent = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<div class={styles.CreateModalContent}>
|
<div class={styles.CreateModalContent}>
|
||||||
<h4>{t('create_chat')}</h4>
|
<h4>{t('create_chat')}</h4>
|
||||||
{uids().length > 1 && (
|
{usersId().length > 1 && (
|
||||||
<input
|
<input
|
||||||
ref={textInput}
|
ref={textInput}
|
||||||
onInput={handleSetTheme}
|
onInput={handleSetTheme}
|
||||||
|
@ -90,7 +80,7 @@ const CreateModalContent = (props: Props) => {
|
||||||
|
|
||||||
<div class="invite-recipients" style={{ height: '400px', overflow: 'auto' }}>
|
<div class="invite-recipients" style={{ height: '400px', overflow: 'auto' }}>
|
||||||
<For each={collectionToInvite()}>
|
<For each={collectionToInvite()}>
|
||||||
{(author: InvitingUser) => (
|
{(author) => (
|
||||||
<InviteUser onClick={() => handleClick(author)} author={author} selected={author.selected} />
|
<InviteUser onClick={() => handleClick(author)} author={author} selected={author.selected} />
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
@ -104,9 +94,9 @@ const CreateModalContent = (props: Props) => {
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-lg fs-3 btn-outline-primary"
|
class="btn btn-lg fs-3 btn-outline-primary"
|
||||||
onClick={handleCreate}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 3px solid #fff;
|
border: 3px solid #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.imageHolder {
|
.imageHolder {
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -29,9 +30,6 @@
|
||||||
.letter {
|
.letter {
|
||||||
display: block;
|
display: block;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
.letter {
|
|
||||||
margin-bottom: -2px;
|
margin-bottom: -2px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
@ -47,4 +45,8 @@
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.bordered {
|
||||||
|
border: 2px solid #fff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ type Props = {
|
||||||
url?: string
|
url?: string
|
||||||
online?: boolean
|
online?: boolean
|
||||||
size?: 'small'
|
size?: 'small'
|
||||||
|
bordered?: boolean
|
||||||
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const colors = [
|
const colors = [
|
||||||
|
@ -36,8 +38,9 @@ const DialogAvatar = (props: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={clsx(styles.DialogAvatar, {
|
class={clsx(styles.DialogAvatar, props.className, {
|
||||||
[styles.online]: props.online,
|
[styles.online]: props.online,
|
||||||
|
[styles.bordered]: props.bordered,
|
||||||
[styles.small]: props.size === 'small'
|
[styles.small]: props.size === 'small'
|
||||||
})}
|
})}
|
||||||
style={{ 'background-color': `${randomBg()}` }}
|
style={{ 'background-color': `${randomBg()}` }}
|
||||||
|
|
|
@ -6,9 +6,10 @@
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
transition: background 0.3s ease-in-out;
|
transition: background 0.3s ease-in-out;
|
||||||
cursor: pointer;
|
width: 100%;
|
||||||
|
|
||||||
&:hover {
|
&.hovered:hover {
|
||||||
|
cursor: pointer;
|
||||||
background: #f7f7f7;
|
background: #f7f7f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +25,7 @@
|
||||||
|
|
||||||
.name,
|
.name,
|
||||||
.message {
|
.message {
|
||||||
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
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 DialogAvatar from './DialogAvatar'
|
||||||
import type { Author, ChatMember, User } from '../../graphql/types.gen'
|
import type { ChatMember } from '../../graphql/types.gen'
|
||||||
import { t } from '../../utils/intl'
|
import GroupDialogAvatar from './GroupDialogAvatar'
|
||||||
import { Show } from 'solid-js'
|
import formattedTime from '../../utils/formatDateTime'
|
||||||
import { useSession } from '../../context/session'
|
import { clsx } from 'clsx'
|
||||||
|
import styles from './DialogCard.module.scss'
|
||||||
|
|
||||||
type DialogProps = {
|
type DialogProps = {
|
||||||
online?: boolean
|
online?: boolean
|
||||||
message?: string
|
message?: string
|
||||||
counter?: number
|
counter?: number
|
||||||
|
title?: string
|
||||||
|
ownId: number
|
||||||
members: ChatMember[]
|
members: ChatMember[]
|
||||||
|
onClick?: () => void
|
||||||
|
isChatHeader?: boolean
|
||||||
|
lastUpdate?: number
|
||||||
|
isOpened?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const DialogCard = (props: DialogProps) => {
|
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 (
|
return (
|
||||||
//DialogCardView - подумать
|
<Show when={props.members}>
|
||||||
<Show when={props.members?.length > 0}>
|
<div
|
||||||
<div class={styles.DialogCard}>
|
class={clsx(styles.DialogCard, {
|
||||||
|
[styles.header]: props.isChatHeader,
|
||||||
|
[styles.opened]: props.isOpened,
|
||||||
|
[styles.hovered]: !props.isChatHeader
|
||||||
|
})}
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
<div class={styles.avatar}>
|
<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>
|
||||||
<div class={styles.row}>
|
<div class={styles.row}>
|
||||||
<div class={styles.name}>{props.members[0].name}</div>
|
<div class={styles.name}>{props.title}</div>
|
||||||
<div class={styles.message}>{t('You can announce your languages in profile')}</div>
|
<div class={styles.message}>
|
||||||
</div>
|
<Switch>
|
||||||
<div class={styles.activity}>
|
<Match when={props.message && !props.isChatHeader}>{props.message}</Match>
|
||||||
<div class={styles.time}>22:22</div>
|
<Match when={props.isChatHeader && companions().length > 1}>{names()}</Match>
|
||||||
<div class={styles.counter}>
|
</Switch>
|
||||||
<span>12</span>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</Show>
|
</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 {
|
.Message {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 3.2rem 0;
|
margin: 3.2rem 0;
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
background: #f6f6f6;
|
|
||||||
font-size: 14px;
|
|
||||||
max-width: 60%;
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
p {
|
.text {
|
||||||
margin: 0;
|
display: inline-flex;
|
||||||
}
|
flex-direction: column;
|
||||||
a {
|
max-width: 60%;
|
||||||
color: inherit;
|
margin-right: auto;
|
||||||
text-decoration: underline;
|
background: #f6f6f6;
|
||||||
&:hover {
|
font-size: 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
word-wrap: break-word;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
color: inherit;
|
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;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.own {
|
&.own {
|
||||||
.body {
|
.body {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-left: auto;
|
|
||||||
background: #000;
|
.text {
|
||||||
color: #fff;
|
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 {
|
.time {
|
||||||
text-align: right;
|
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 MarkdownIt from 'markdown-it'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import styles from './Message.module.scss'
|
import styles from './Message.module.scss'
|
||||||
import DialogAvatar from './DialogAvatar'
|
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 = {
|
type Props = {
|
||||||
body: string
|
content: Message
|
||||||
isOwn: boolean
|
ownId: number
|
||||||
|
members: ChatMember[]
|
||||||
|
replyClick?: () => void
|
||||||
|
replyBody?: string
|
||||||
|
replyAuthor?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const md = new MarkdownIt({
|
const md = new MarkdownIt({
|
||||||
linkify: true
|
linkify: true,
|
||||||
|
breaks: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const Message = (props: Props) => {
|
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 (
|
return (
|
||||||
<div class={clsx(styles.Message, props.isOwn && styles.own)}>
|
<div class={clsx(styles.Message, isOwn && styles.own)}>
|
||||||
<Show when={!props.isOwn}>
|
<Show when={!isOwn}>
|
||||||
<div class={styles.author}>
|
<div class={styles.author}>
|
||||||
<DialogAvatar size="small" name={'Message Author'} />
|
<DialogAvatar size="small" name={user.name} url={user.userpic} />
|
||||||
<div class={styles.name}>Message Author</div>
|
<div class={styles.name}>{user.name}</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class={styles.body}>
|
<div class={clsx(styles.body, { [styles.popupVisible]: isPopupVisible() })}>
|
||||||
<div innerHTML={md.render(props.body)} />
|
<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>
|
||||||
<div class={styles.time}>12:24</div>
|
<div class={styles.time}>{formattedTime(props.content.createdAt * 1000)}</div>
|
||||||
</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 = {
|
export type ConfirmEmailSearchParams = {
|
||||||
token: string
|
token: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CreateChatSearchParams = {
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { clsx } from 'clsx'
|
||||||
import { useRouter } from '../../stores/router'
|
import { useRouter } from '../../stores/router'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import { createEffect, createSignal, Show } from 'solid-js'
|
import { createSignal, Show } from 'solid-js'
|
||||||
import Notifications from './Notifications'
|
import Notifications from './Notifications'
|
||||||
import { ProfilePopup } from './ProfilePopup'
|
import { ProfilePopup } from './ProfilePopup'
|
||||||
import Userpic from '../Author/Userpic'
|
import Userpic from '../Author/Userpic'
|
||||||
|
|
|
@ -17,7 +17,7 @@ interface ModalProps {
|
||||||
export const Modal = (props: ModalProps) => {
|
export const Modal = (props: ModalProps) => {
|
||||||
const { modal } = useModalStore()
|
const { modal } = useModalStore()
|
||||||
|
|
||||||
const backdropClick = (event: Event) => {
|
const backdropClick = () => {
|
||||||
hideModal()
|
hideModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ export const ProfilePopup = (props: ProfilePopupProps) => {
|
||||||
} = useSession()
|
} = useSession()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popup {...props} horizontalAnchor="right">
|
<Popup {...props} horizontalAnchor="right" variant="bordered">
|
||||||
{/*TODO: l10n*/}
|
{/*TODO: l10n*/}
|
||||||
<ul class="nodash">
|
<ul class="nodash">
|
||||||
<li>
|
<li>
|
||||||
|
|
|
@ -72,7 +72,7 @@ export const ProfileSecurityPage = (props: PageProps) => {
|
||||||
<h5>Google</h5>
|
<h5>Google</h5>
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<p>
|
<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} />
|
<Icon name="google" class={styles.icon} />
|
||||||
Привязать
|
Привязать
|
||||||
</button>
|
</button>
|
||||||
|
@ -82,7 +82,7 @@ export const ProfileSecurityPage = (props: PageProps) => {
|
||||||
<h5>VK</h5>
|
<h5>VK</h5>
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<p>
|
<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} />
|
<Icon name="vk" class={styles.icon} />
|
||||||
Привязать
|
Привязать
|
||||||
</button>
|
</button>
|
||||||
|
@ -92,7 +92,7 @@ export const ProfileSecurityPage = (props: PageProps) => {
|
||||||
<h5>Facebook</h5>
|
<h5>Facebook</h5>
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<p>
|
<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} />
|
<Icon name="facebook" class={styles.icon} />
|
||||||
Привязать
|
Привязать
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -9,6 +9,7 @@ h4 {
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
@include font-size(1.7rem);
|
@include font-size(1.7rem);
|
||||||
|
|
||||||
margin: 0 0 0.8rem;
|
margin: 0 0 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { capitalize } from '../../utils'
|
import { capitalize } from '../../utils'
|
||||||
import styles from './Card.module.scss'
|
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 type { Topic } from '../../graphql/types.gen'
|
||||||
import { FollowingEntity } from '../../graphql/types.gen'
|
import { FollowingEntity } from '../../graphql/types.gen'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
|
@ -8,7 +8,6 @@ import { follow, unfollow } from '../../stores/zine/common'
|
||||||
import { getLogger } from '../../utils/logger'
|
import { getLogger } from '../../utils/logger'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import { StatMetrics } from '../_shared/StatMetrics'
|
|
||||||
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { createEffect, createMemo, createSignal, onMount, Show, Suspense } from
|
||||||
import { FullArticle } from '../Article/FullArticle'
|
import { FullArticle } from '../Article/FullArticle'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import type { Shout, Reaction } from '../../graphql/types.gen'
|
import type { Shout, Reaction } from '../../graphql/types.gen'
|
||||||
import { useReactionsStore } from '../../stores/zine/reactions'
|
|
||||||
|
|
||||||
interface ArticlePageProps {
|
interface ArticlePageProps {
|
||||||
article: Shout
|
article: Shout
|
||||||
|
|
|
@ -1,51 +1,27 @@
|
||||||
import { For, createSignal, Show, onMount, createEffect, createMemo } from 'solid-js'
|
import { For, createSignal, Show, onMount, createEffect, createMemo } from 'solid-js'
|
||||||
import type { Author, Chat, ChatMember } from '../../graphql/types.gen'
|
import type { Author, Chat, Message as MessageType } from '../../graphql/types.gen'
|
||||||
import { AuthorCard } from '../Author/Card'
|
|
||||||
import { Icon } from '../_shared/Icon'
|
|
||||||
import { Loading } from '../Loading'
|
|
||||||
import DialogCard from '../Inbox/DialogCard'
|
import DialogCard from '../Inbox/DialogCard'
|
||||||
import Search from '../Inbox/Search'
|
import Search from '../Inbox/Search'
|
||||||
import { useSession } from '../../context/session'
|
|
||||||
import { createClient } from '@urql/core'
|
|
||||||
import Message from '../Inbox/Message'
|
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 { t } from '../../utils/intl'
|
||||||
import '../../styles/Inbox.scss'
|
|
||||||
import { useInbox } from '../../context/inbox'
|
|
||||||
import { Modal } from '../Nav/Modal'
|
import { Modal } from '../Nav/Modal'
|
||||||
import { showModal } from '../../stores/ui'
|
import { showModal } from '../../stores/ui'
|
||||||
import InviteUser from '../Inbox/InviteUser'
|
import { useInbox } from '../../context/inbox'
|
||||||
import CreateModalContent from '../Inbox/CreateModalContent'
|
import { useRouter } from '../../stores/router'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import styles from '../../styles/Inbox.module.scss'
|
||||||
|
|
||||||
const OWNER_ID = '501'
|
type InboxSearchParams = {
|
||||||
const client = createClient({
|
initChat: string
|
||||||
url: 'https://graphqlzero.almansi.me/api'
|
chat: string
|
||||||
})
|
|
||||||
|
|
||||||
const messageQuery = `
|
|
||||||
query Comments ($options: PageQueryOptions) {
|
|
||||||
comments(options: $options) {
|
|
||||||
data {
|
|
||||||
id
|
|
||||||
body
|
|
||||||
email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`
|
|
||||||
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 userSearch = (array: Author[], keyword: string) => {
|
||||||
const searchTerm = keyword.toLowerCase()
|
const searchTerm = keyword.toLowerCase()
|
||||||
return array.filter((value) => {
|
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 = () => {
|
export const InboxView = () => {
|
||||||
const [messages, setMessages] = createSignal([])
|
const {
|
||||||
const [recipients, setRecipients] = createSignal<Author[]>([])
|
chats,
|
||||||
const [chats, setChats] = createSignal<Chat[]>([])
|
messages,
|
||||||
const [cashedRecipients, setCashedRecipients] = createSignal<Author[]>([])
|
actions: { loadChats, getMessages, sendMessage, createChat }
|
||||||
const [postMessageText, setPostMessageText] = createSignal('')
|
} = useInbox()
|
||||||
const [loading, setLoading] = createSignal<boolean>(false)
|
|
||||||
const { session } = useSession()
|
|
||||||
|
|
||||||
|
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) => {
|
const getQuery = (query) => {
|
||||||
if (query().length >= 2) {
|
// if (query().length >= 2) {
|
||||||
const match = userSearch(recipients(), query())
|
// const match = userSearch(recipients(), query())
|
||||||
setRecipients(match)
|
// setRecipients(match)
|
||||||
} else {
|
// } else {
|
||||||
setRecipients(cashedRecipients())
|
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let chatWindow
|
let chatWindow
|
||||||
onMount(async () => {
|
|
||||||
setLoading(true)
|
const handleOpenChat = async (chat: Chat) => {
|
||||||
|
setCurrentDialog(chat)
|
||||||
|
changeSearchParam('chat', `${chat.id}`)
|
||||||
try {
|
try {
|
||||||
await fetchMessages(messageQuery)
|
await getMessages(chat.id)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setLoading(false)
|
console.error('[getMessages]', error)
|
||||||
console.error([fetchMessages], error)
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
|
||||||
chatWindow.scrollTop = chatWindow.scrollHeight
|
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 {
|
try {
|
||||||
const response = await loadRecipients({ days: 365 })
|
const response = await loadRecipients({ days: 365 })
|
||||||
setRecipients(response as unknown as Author[])
|
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) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
|
await loadChats()
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
await sendMessage({
|
||||||
const post = await postMessage(postMessageText())
|
body: postMessageText().toString(),
|
||||||
setMessages((prev) => [...prev, post])
|
chat: currentDialog().id.toString(),
|
||||||
setPostMessageText('')
|
replyTo: messageToReply()?.id
|
||||||
chatWindow.scrollTop = chatWindow.scrollHeight
|
})
|
||||||
} catch (error) {
|
setPostMessageText('')
|
||||||
console.error('[post message error]:', error)
|
setMessageToReply(null)
|
||||||
}
|
chatWindow.scrollTop = chatWindow.scrollHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
let textareaParent // textarea autoresize ghost element
|
let textareaParent // textarea autoresize ghost element
|
||||||
const handleChangeMessage = (event) => {
|
const handleChangeMessage = (event) => {
|
||||||
setPostMessageText(event.target.value)
|
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) => {
|
const handleOpenInviteModal = () => {
|
||||||
event.preventDefault()
|
|
||||||
showModal('inviteToChat')
|
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 (
|
return (
|
||||||
<div class="messages container">
|
<div class={clsx('container', styles.Inbox)}>
|
||||||
<Modal variant="narrow" name="inviteToChat">
|
<Modal variant="narrow" name="inviteToChat">
|
||||||
<CreateModalContent users={recipients()} />
|
<CreateModalContent users={recipients()} />
|
||||||
</Modal>
|
</Modal>
|
||||||
<div class="row">
|
<div class={clsx('row', styles.row)}>
|
||||||
<div class="chat-list col-md-4">
|
<div class={clsx(styles.chatList, 'col-md-4')}>
|
||||||
<div class="sidebar-header">
|
<div class={styles.sidebarHeader}>
|
||||||
<Search placeholder="Поиск" onChange={getQuery} />
|
<Search placeholder="Поиск" onChange={getQuery} />
|
||||||
<div onClick={handleOpenInviteModal}>
|
<button type="button" onClick={handleOpenInviteModal}>
|
||||||
<Icon name="plus-button" style={{ width: '40px', height: '40px' }} />
|
<Icon name="plus-button" style={{ width: '40px', height: '40px' }} />
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chat-list__types">
|
<Show when={chatsToShow}>
|
||||||
<ul>
|
<div class={styles.chatListTypes}>
|
||||||
<li>
|
<ul>
|
||||||
<strong>{t('All')}</strong>
|
<li
|
||||||
</li>
|
class={clsx({ [styles.selected]: !sortByPerToPer() && !sortByGroup() })}
|
||||||
<li>{t('Personal')}</li>
|
onClick={() => {
|
||||||
<li>{t('Groups')}</li>
|
setSortByPerToPer(false)
|
||||||
</ul>
|
setSortByGroup(false)
|
||||||
</div>
|
}}
|
||||||
<div class="holder">
|
>
|
||||||
<div class="dialogs">
|
<span>{t('All')}</span>
|
||||||
<For each={chats()}>{(chat: Chat) => <DialogCard members={chat.members} />}</For>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8 conversation">
|
<div class={clsx('col-md-8', styles.conversation)}>
|
||||||
<div class="interlocutor user--online">
|
<Show
|
||||||
<AuthorCard author={{} as Author} hideFollow={true} />
|
when={currentDialog()}
|
||||||
<div class="user-status">Online</div>
|
fallback={
|
||||||
</div>
|
<MessagesFallback
|
||||||
|
message={t('Choose who you want to write to')}
|
||||||
<div class="conversation__messages">
|
onClick={handleOpenInviteModal}
|
||||||
<div class="conversation__messages-container" ref={chatWindow}>
|
actionText={t('Start conversation')}
|
||||||
<Show when={loading()}>
|
/>
|
||||||
<Loading />
|
}
|
||||||
</Show>
|
>
|
||||||
<For each={messages()}>
|
<DialogHeader ownId={currentUserId()} chat={currentDialog()} />
|
||||||
{(comment: { body: string; id: string; email: string }) => (
|
<div class={styles.conversationMessages}>
|
||||||
<Message body={comment.body} isOwn={OWNER_ID === comment.id} />
|
<div class={styles.messagesContainer} ref={chatWindow}>
|
||||||
)}
|
<For each={messages()}>
|
||||||
</For>
|
{(message) => (
|
||||||
|
<Message
|
||||||
{/*<div class="conversation__date">*/}
|
content={message}
|
||||||
{/* <time>12 сентября</time>*/}
|
ownId={currentUserId()}
|
||||||
{/*</div>*/}
|
members={currentDialog().members}
|
||||||
</div>
|
replyBody={message.replyTo && findToReply(message.replyTo).body}
|
||||||
</div>
|
replyClick={() => setMessageToReply(message)}
|
||||||
|
/>
|
||||||
<div class="message-form">
|
)}
|
||||||
<div class="wrapper">
|
</For>
|
||||||
<div class="grow-wrap" ref={textareaParent}>
|
{/*<div class={styles.conversationDate}>*/}
|
||||||
<textarea
|
{/* <time>12 сентября</time>*/}
|
||||||
value={postMessageText()}
|
{/*</div>*/}
|
||||||
rows={1}
|
|
||||||
onInput={(event) => handleChangeMessage(event)}
|
|
||||||
placeholder="Написать сообщение"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" disabled={postMessageText().length === 0} onClick={handleSubmit}>
|
|
||||||
<Icon name="send-message" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,9 +4,49 @@
|
||||||
|
|
||||||
.popup {
|
.popup {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 2px solid #000;
|
|
||||||
top: calc(100% + 8px);
|
top: calc(100% + 8px);
|
||||||
opacity: 1;
|
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 {
|
&.horizontalAnchorCenter {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
@ -17,25 +57,6 @@
|
||||||
right: 0;
|
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 {
|
.topBorderItem {
|
||||||
border-top: 2px solid;
|
border-top: 2px solid;
|
||||||
padding-top: 1em;
|
padding-top: 1em;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { createEffect, createSignal, JSX, Show } from 'solid-js'
|
||||||
import styles from './Popup.module.scss'
|
import styles from './Popup.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
|
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
|
||||||
|
import { set } from 'husky'
|
||||||
|
|
||||||
type HorizontalAnchor = 'center' | 'right'
|
type HorizontalAnchor = 'center' | 'right'
|
||||||
|
|
||||||
|
@ -11,6 +12,7 @@ export type PopupProps = {
|
||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
onVisibilityChange?: (isVisible) => void
|
onVisibilityChange?: (isVisible) => void
|
||||||
horizontalAnchor?: HorizontalAnchor
|
horizontalAnchor?: HorizontalAnchor
|
||||||
|
variant?: 'bordered' | 'tiny'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Popup = (props: PopupProps) => {
|
export const Popup = (props: PopupProps) => {
|
||||||
|
@ -40,7 +42,9 @@ export const Popup = (props: PopupProps) => {
|
||||||
<div
|
<div
|
||||||
class={clsx(styles.popup, {
|
class={clsx(styles.popup, {
|
||||||
[styles.horizontalAnchorCenter]: horizontalAnchor === 'center',
|
[styles.horizontalAnchorCenter]: horizontalAnchor === 'center',
|
||||||
[styles.horizontalAnchorRight]: horizontalAnchor === 'right'
|
[styles.horizontalAnchorRight]: horizontalAnchor === 'right',
|
||||||
|
[styles.bordered]: props.variant === 'bordered',
|
||||||
|
[styles.tiny]: props.variant === 'tiny'
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import styles from './SearchField.module.scss'
|
import styles from './SearchField.module.scss'
|
||||||
import { Icon } from './Icon'
|
import { Icon } from './Icon'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import clsx from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
type SearchFieldProps = {
|
type SearchFieldProps = {
|
||||||
onChange: (value: string) => void
|
onChange: (value: string) => void
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'swiper/scss'
|
||||||
import 'swiper/scss/navigation'
|
import 'swiper/scss/navigation'
|
||||||
import 'swiper/scss/pagination'
|
import 'swiper/scss/pagination'
|
||||||
import './Slider.scss'
|
import './Slider.scss'
|
||||||
import { createEffect, createMemo, createSignal, Show, For, JSX } from 'solid-js'
|
import { createEffect, createSignal, JSX } from 'solid-js'
|
||||||
import { Icon } from './Icon'
|
import { Icon } from './Icon'
|
||||||
|
|
||||||
interface SliderProps {
|
interface SliderProps {
|
||||||
|
|
|
@ -1,13 +1,22 @@
|
||||||
import type { JSX } from 'solid-js'
|
import { createContext, createSignal, useContext } from 'solid-js'
|
||||||
import { createContext, useContext } from 'solid-js'
|
import type { Accessor, JSX } from 'solid-js'
|
||||||
import type { Message } from '../graphql/types.gen'
|
// import { createChatClient } from '../graphql/privateGraphQLClient'
|
||||||
|
import type { Chat, Message, MutationCreateMessageArgs } from '../graphql/types.gen'
|
||||||
import { apiClient } from '../utils/apiClient'
|
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 = {
|
type InboxContextType = {
|
||||||
chatEntities: { [chatId: string]: Message[] }
|
chats: Accessor<Chat[]>
|
||||||
|
messages?: Accessor<Message[]>
|
||||||
actions: {
|
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 }) => {
|
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 createChat = async (members: number[], title: string) => {
|
||||||
const chat = await apiClient.createChat({ members, title })
|
const chat = await apiClient.createChat({ members, title })
|
||||||
setChatEntities((s) => {
|
setChats((prevChats) => {
|
||||||
s[chat.id] = chat
|
return [chat, ...prevChats]
|
||||||
})
|
})
|
||||||
return chat
|
return chat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { unsubscribe } = pipe(
|
||||||
|
() => null, // subclient().subscription(newMessage, {}),
|
||||||
|
subscribe((result) => {
|
||||||
|
console.info('[subscription]')
|
||||||
|
console.debug(result)
|
||||||
|
// TODO: handle data result
|
||||||
|
})
|
||||||
|
)
|
||||||
const actions = {
|
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>
|
return <InboxContext.Provider value={value}>{props.children}</InboxContext.Provider>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation createMessage($chat: String!, $body: String!) {
|
mutation createMessage($chat: String!, $body: String!, $replyTo: Int) {
|
||||||
createMessage(chat: $chat, body: $body) {
|
createMessage(chat: $chat, body: $body, replyTo: $replyTo) {
|
||||||
error
|
error
|
||||||
author {
|
message {
|
||||||
slug
|
|
||||||
id
|
id
|
||||||
|
body
|
||||||
|
author
|
||||||
|
createdAt
|
||||||
|
replyTo
|
||||||
|
updatedAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ export default gql`
|
||||||
token
|
token
|
||||||
user {
|
user {
|
||||||
_id: slug
|
_id: slug
|
||||||
|
id
|
||||||
name
|
name
|
||||||
slug
|
slug
|
||||||
bio
|
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 { devtoolsExchange } from '@urql/devtools'
|
||||||
import { isDev, apiBaseUrl } from '../utils/config'
|
import { isDev, apiBaseUrl } from '../utils/config'
|
||||||
// import { cache } from './cache'
|
// import { cache } from './cache'
|
||||||
|
@ -28,7 +37,7 @@ export const resetToken = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: ClientOptions = {
|
const options: ClientOptions = {
|
||||||
url: apiBaseUrl,
|
url: apiBaseUrl + '/graphql',
|
||||||
maskTypename: true,
|
maskTypename: true,
|
||||||
requestPolicy: 'cache-and-network',
|
requestPolicy: 'cache-and-network',
|
||||||
fetchOptions: () => {
|
fetchOptions: () => {
|
||||||
|
@ -45,3 +54,25 @@ const options: ClientOptions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const privateGraphQLClient = createClient(options)
|
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 = {
|
const options: ClientOptions = {
|
||||||
url: apiBaseUrl,
|
url: apiBaseUrl + '/graphql',
|
||||||
maskTypename: true,
|
maskTypename: true,
|
||||||
requestPolicy: 'cache-and-network',
|
requestPolicy: 'cache-and-network',
|
||||||
exchanges
|
exchanges
|
||||||
|
|
|
@ -4,6 +4,7 @@ export default gql`
|
||||||
query GetAuthorBySlugQuery($slug: String!) {
|
query GetAuthorBySlugQuery($slug: String!) {
|
||||||
getAuthor(slug: $slug) {
|
getAuthor(slug: $slug) {
|
||||||
_id: slug
|
_id: slug
|
||||||
|
id
|
||||||
slug
|
slug
|
||||||
name
|
name
|
||||||
bio
|
bio
|
||||||
|
|
|
@ -4,6 +4,7 @@ export default gql`
|
||||||
query UserSubscribersQuery($slug: String!) {
|
query UserSubscribersQuery($slug: String!) {
|
||||||
userSubcribers(slug: $slug) {
|
userSubcribers(slug: $slug) {
|
||||||
_id: slug
|
_id: slug
|
||||||
|
id
|
||||||
slug
|
slug
|
||||||
name
|
name
|
||||||
bio
|
bio
|
||||||
|
|
|
@ -4,6 +4,7 @@ export default gql`
|
||||||
query UserFollowingQuery($slug: String!) {
|
query UserFollowingQuery($slug: String!) {
|
||||||
userFollowing(slug: $slug) {
|
userFollowing(slug: $slug) {
|
||||||
_id: slug
|
_id: slug
|
||||||
|
id
|
||||||
slug
|
slug
|
||||||
name
|
name
|
||||||
bio
|
bio
|
||||||
|
|
|
@ -4,6 +4,7 @@ export default gql`
|
||||||
query AuthorsAllQuery {
|
query AuthorsAllQuery {
|
||||||
authorsAll {
|
authorsAll {
|
||||||
_id: slug
|
_id: slug
|
||||||
|
id
|
||||||
slug
|
slug
|
||||||
name
|
name
|
||||||
bio
|
bio
|
||||||
|
|
|
@ -4,6 +4,7 @@ export default gql`
|
||||||
query AuthorLoadByQuery($by: AuthorsBy, $limit: Int, $offset: Int) {
|
query AuthorLoadByQuery($by: AuthorsBy, $limit: Int, $offset: Int) {
|
||||||
loadAuthorsBy(by: $by, limit: $limit, offset: $offset) {
|
loadAuthorsBy(by: $by, limit: $limit, offset: $offset) {
|
||||||
_id: slug
|
_id: slug
|
||||||
|
id
|
||||||
slug
|
slug
|
||||||
name
|
name
|
||||||
bio
|
bio
|
||||||
|
|
|
@ -7,9 +7,11 @@ export default gql`
|
||||||
messages {
|
messages {
|
||||||
author
|
author
|
||||||
body
|
body
|
||||||
|
replyTo
|
||||||
createdAt
|
createdAt
|
||||||
|
id
|
||||||
updatedAt
|
updatedAt
|
||||||
seen
|
replyTo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ export default gql`
|
||||||
members {
|
members {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
id
|
||||||
slug
|
slug
|
||||||
userpic
|
userpic
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,19 @@ export default gql`
|
||||||
error
|
error
|
||||||
chats {
|
chats {
|
||||||
id
|
id
|
||||||
|
title
|
||||||
admins
|
admins
|
||||||
users
|
users
|
||||||
|
members {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
userpic
|
||||||
|
}
|
||||||
unread
|
unread
|
||||||
description
|
description
|
||||||
updatedAt
|
updatedAt
|
||||||
|
private
|
||||||
messages {
|
messages {
|
||||||
id
|
id
|
||||||
body
|
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']
|
chatId: Scalars['String']
|
||||||
createdAt: Scalars['Int']
|
createdAt: Scalars['Int']
|
||||||
id: Scalars['Int']
|
id: Scalars['Int']
|
||||||
replyTo?: Maybe<Scalars['String']>
|
replyTo?: Maybe<Scalars['Int']>
|
||||||
seen?: Maybe<Scalars['Boolean']>
|
seen?: Maybe<Scalars['Boolean']>
|
||||||
updatedAt?: Maybe<Scalars['Int']>
|
updatedAt?: Maybe<Scalars['Int']>
|
||||||
}
|
}
|
||||||
|
|
|
@ -184,12 +184,13 @@
|
||||||
"discussion": "дискурс",
|
"discussion": "дискурс",
|
||||||
"Personal": "Личные",
|
"Personal": "Личные",
|
||||||
"Groups": "Группы",
|
"Groups": "Группы",
|
||||||
"All": "Все",
|
|
||||||
"create_chat": "Создать чат",
|
"create_chat": "Создать чат",
|
||||||
"create_group": "Создать группу",
|
"create_group": "Создать группу",
|
||||||
"discourse_theme": "Тема дискурса",
|
"discourse_theme": "Тема дискурса",
|
||||||
"cancel": "Отмена",
|
"cancel": "Отмена",
|
||||||
"group_chat": "Общий чат",
|
"group_chat": "Общий чат",
|
||||||
|
"Choose who you want to write to": "Выберите кому хотите написать",
|
||||||
|
"Start conversation": "Начать беседу",
|
||||||
"Profile settings": "Настройки профиля",
|
"Profile settings": "Настройки профиля",
|
||||||
"Here you can customize your profile the way you want.": "Здесь можно настроить свой профиль так, как вы хотите.",
|
"Here you can customize your profile the way you want.": "Здесь можно настроить свой профиль так, как вы хотите.",
|
||||||
"Userpic": "Аватар",
|
"Userpic": "Аватар",
|
||||||
|
@ -204,6 +205,11 @@
|
||||||
"Date of Birth": "Дата рождения",
|
"Date of Birth": "Дата рождения",
|
||||||
"Social networks": "Социальные сети",
|
"Social networks": "Социальные сети",
|
||||||
"Save settings": "Сохранить настройки",
|
"Save settings": "Сохранить настройки",
|
||||||
|
"Write message": "Написать сообщение",
|
||||||
|
"Copy": "Скопировать",
|
||||||
|
"Pin": "Закрепить",
|
||||||
|
"Forward": "Переслать",
|
||||||
|
"Select": "Выбрать",
|
||||||
"slug is used by another user": "Имя уже занято другим пользователем",
|
"slug is used by another user": "Имя уже занято другим пользователем",
|
||||||
"It does not look like url": "Это не похоже на ссылку"
|
"It does not look like url": "Это не похоже на ссылку"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { apiClient } from '../utils/apiClient'
|
import { apiClient } from '../utils/apiClient'
|
||||||
|
import type { MessagesBy } from '../graphql/types.gen'
|
||||||
|
|
||||||
export const loadRecipients = async (by = {}): Promise<void> => {
|
export const loadRecipients = async (by = {}): Promise<void> => {
|
||||||
return await apiClient.getRecipients(by)
|
return await apiClient.getRecipients(by)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadChats = async (): Promise<void> => {
|
export const loadMessages = async (by: MessagesBy): Promise<void> => {
|
||||||
return await apiClient.getChats({ limit: 50, offset: 0 })
|
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'
|
import { apiClient } from '../../utils/apiClient'
|
||||||
|
|
||||||
export const follow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
|
export const follow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
|
||||||
|
console.log('!!! follow:')
|
||||||
await apiClient.follow({ what, slug })
|
await apiClient.follow({ what, slug })
|
||||||
}
|
}
|
||||||
export const unfollow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
|
export const unfollow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
|
||||||
|
|
|
@ -60,6 +60,7 @@ img {
|
||||||
|
|
||||||
.shoutMediaBody {
|
.shoutMediaBody {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
audio {
|
audio {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
@ -258,6 +259,7 @@ img {
|
||||||
|
|
||||||
.commentsHeader {
|
.commentsHeader {
|
||||||
@include font-size(2.4rem);
|
@include font-size(2.4rem);
|
||||||
|
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,6 +289,7 @@ img {
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@include font-size(1.5rem);
|
@include font-size(1.5rem);
|
||||||
|
|
||||||
border-radius: 0.8rem;
|
border-radius: 0.8rem;
|
||||||
margin-right: 1.2rem;
|
margin-right: 1.2rem;
|
||||||
padding: 1.1rem 1.2rem 0.9rem;
|
padding: 1.1rem 1.2rem 0.9rem;
|
||||||
|
|
|
@ -5,17 +5,18 @@ main {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages {
|
.Inbox {
|
||||||
display: none;
|
top: 84px;
|
||||||
top: 74px;
|
|
||||||
height: calc(100% - 74px);
|
height: calc(100% - 74px);
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
padding-left: 42px;
|
padding-left: 42px;
|
||||||
padding-right: 26px;
|
padding-right: 26px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -31,7 +32,7 @@ main {
|
||||||
}
|
}
|
||||||
|
|
||||||
// список диалогов и юзеров
|
// список диалогов и юзеров
|
||||||
.chat-list {
|
.chatList {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
@ -39,7 +40,7 @@ main {
|
||||||
|
|
||||||
$fade-height: 10px;
|
$fade-height: 10px;
|
||||||
|
|
||||||
.sidebar-header {
|
.sidebarHeader {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
@ -56,7 +57,7 @@ main {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
right: 10px;
|
right: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
height: $fade-height;
|
height: $fade-height;
|
||||||
}
|
}
|
||||||
|
@ -87,14 +88,13 @@ main {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-list__search,
|
.chat-list__search {
|
||||||
.interlocutor {
|
|
||||||
border-bottom: 3px solid #141414;
|
border-bottom: 3px solid #141414;
|
||||||
padding: 1em 0;
|
padding: 1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// табы выбора списка
|
// табы выбора списка
|
||||||
.chat-list__types {
|
.chatListTypes {
|
||||||
@include font-size(1.7rem);
|
@include font-size(1.7rem);
|
||||||
|
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
|
@ -110,51 +110,16 @@ main {
|
||||||
li {
|
li {
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
color: #696969;
|
color: #696969;
|
||||||
}
|
cursor: pointer;
|
||||||
|
|
||||||
strong {
|
&.selected {
|
||||||
border-bottom: 3px solid;
|
span {
|
||||||
font-weight: normal;
|
border-bottom: 3px solid;
|
||||||
color: #000;
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 {
|
.conversation {
|
||||||
|
@ -162,36 +127,31 @@ main {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation__messages {
|
.messageForm {
|
||||||
flex: 1;
|
|
||||||
overflow: auto;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-form {
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 2px 0 12px 0;
|
padding: 2px 0 12px;
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
border: 2px solid #cccccc;
|
border: 2px solid #ccc;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.grow-wrap {
|
.growWrap {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: attr(data-replicated-value) ' ';
|
content: attr(data-replicated-value);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: height 1.3s ease-in-out;
|
transition: height 1.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
& textarea {
|
.textInput {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
border: none;
|
border: none;
|
||||||
|
@ -243,36 +203,40 @@ main {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation__messages-container {
|
.conversationMessages {
|
||||||
left: 0;
|
flex: 1;
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversation__date {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
text-align: center;
|
.messagesContainer {
|
||||||
|
|
||||||
&::before {
|
|
||||||
background: #141414;
|
|
||||||
content: '';
|
|
||||||
height: 1px;
|
|
||||||
left: 0;
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.8em;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: -1;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
.conversation__date {
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
time {
|
&::before {
|
||||||
background: #fff;
|
background: #141414;
|
||||||
@include font-size(1.5rem);
|
content: '';
|
||||||
|
height: 1px;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.8em;
|
||||||
|
width: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
color: #9fa1a7;
|
time {
|
||||||
padding: 0 0.5em;
|
@include font-size(1.5rem);
|
||||||
|
|
||||||
|
background: #fff;
|
||||||
|
color: #9fa1a7;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -85,8 +85,8 @@ h2 {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
margin-left: -0.15em;
|
margin-left: -0.15em;
|
||||||
padding: 0 0.15em;
|
padding: 0 0.15em;
|
||||||
box-decoration-break: clone;
|
|
||||||
-webkit-box-decoration-break: clone;
|
-webkit-box-decoration-break: clone;
|
||||||
|
box-decoration-break: clone;
|
||||||
|
|
||||||
&::selection {
|
&::selection {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
@ -809,5 +809,6 @@ details {
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
@include font-size(1.4rem);
|
@include font-size(1.4rem);
|
||||||
|
|
||||||
color: rgba(0 0 0 / 40%);
|
color: rgba(0 0 0 / 40%);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@ import type {
|
||||||
QueryLoadMessagesByArgs,
|
QueryLoadMessagesByArgs,
|
||||||
MutationCreateChatArgs,
|
MutationCreateChatArgs,
|
||||||
MutationCreateMessageArgs,
|
MutationCreateMessageArgs,
|
||||||
|
Chat,
|
||||||
QueryLoadRecipientsArgs,
|
QueryLoadRecipientsArgs,
|
||||||
User,
|
|
||||||
ProfileInput
|
ProfileInput
|
||||||
} from '../graphql/types.gen'
|
} from '../graphql/types.gen'
|
||||||
import { publicGraphQLClient } from '../graphql/publicGraphQLClient'
|
import { publicGraphQLClient } from '../graphql/publicGraphQLClient'
|
||||||
|
@ -276,7 +276,7 @@ export const apiClient = {
|
||||||
},
|
},
|
||||||
|
|
||||||
// inbox
|
// inbox
|
||||||
getChats: async (options: QueryLoadChatsArgs) => {
|
getChats: async (options: QueryLoadChatsArgs): Promise<Chat[]> => {
|
||||||
const resp = await privateGraphQLClient.query(myChats, options).toPromise()
|
const resp = await privateGraphQLClient.query(myChats, options).toPromise()
|
||||||
return resp.data.loadChats.chats
|
return resp.data.loadChats.chats
|
||||||
},
|
},
|
||||||
|
@ -288,13 +288,15 @@ 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
|
console.log('[getChatMessages]', resp)
|
||||||
|
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()
|
||||||
return resp.data.loadRecipients.members
|
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(
|
return arr.reduce(
|
||||||
(acc, tt) => {
|
(acc, tt) => {
|
||||||
let c = (tt.name || '')
|
let c = (tt.name || '')
|
||||||
.replace(/[^\d A-Za-zА-я]/g, '')
|
.replaceAll(/[^\d A-Za-zА-я]/g, '')
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.pop()
|
.pop()
|
||||||
.slice(0, 1)
|
.slice(0, 1)
|
||||||
|
@ -24,7 +24,7 @@ export const groupByTitle = (arr: (Shout | Topic)[]) => {
|
||||||
return arr.reduce(
|
return arr.reduce(
|
||||||
(acc, tt) => {
|
(acc, tt) => {
|
||||||
let c = (tt.title || '')
|
let c = (tt.title || '')
|
||||||
.replace(/[^\d A-Za-zА-я]/g, '')
|
.replaceAll(/[^\d A-Za-zА-я]/g, '')
|
||||||
.slice(0, 1)
|
.slice(0, 1)
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
if (/[^А-я]/.test(c)) c = 'A-Z'
|
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"
|
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
|
||||||
integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
|
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:
|
commander@^9.3.0:
|
||||||
version "9.4.1"
|
version "9.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.1.tgz#d1dd8f2ce6faf93147295c0df13c7c21141cfbdd"
|
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"
|
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.15.tgz#5a277ce10de999d2a6465fc92a8c2a2d207ebd31"
|
||||||
integrity sha512-ttuoCYCIJAFx4UUKKWYnFdrVpoXa3+3WWkXVI6s09U+YjhnyM5h96ewTq/WgQj9LFSIlABQvadHSOQyAVjW5xQ==
|
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"
|
version "0.14.54"
|
||||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2"
|
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2"
|
||||||
integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==
|
integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==
|
||||||
|
@ -5949,7 +5944,7 @@ glob-parent@^6.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-glob "^4.0.3"
|
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"
|
version "7.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
||||||
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
|
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
|
||||||
|
@ -6085,6 +6080,11 @@ graphql-request@^5.0.0:
|
||||||
extract-files "^9.0.0"
|
extract-files "^9.0.0"
|
||||||
form-data "^3.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:
|
graphql-tag@^2.11.0, graphql-tag@^2.12.6:
|
||||||
version "2.12.6"
|
version "2.12.6"
|
||||||
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1"
|
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"
|
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
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"
|
version "8.4.19"
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.19.tgz#61178e2add236b17351897c8bcc0b4c8ecab56fc"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.19.tgz#61178e2add236b17351897c8bcc0b4c8ecab56fc"
|
||||||
integrity sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==
|
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"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
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:
|
pvtsutils@^1.3.2:
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.2.tgz#9f8570d132cdd3c27ab7d51a2799239bf8d8d5de"
|
resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.2.tgz#9f8570d132cdd3c27ab7d51a2799239bf8d8d5de"
|
||||||
|
@ -9624,13 +9614,6 @@ rollup-pluginutils@^2.8.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
estree-walker "^0.6.1"
|
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:
|
rollup@^2.79.1:
|
||||||
version "2.79.1"
|
version "2.79.1"
|
||||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7"
|
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"
|
unist-util-stringify-position "^3.0.0"
|
||||||
vfile-message "^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:
|
vite@^3.2.4, vite@~3.2.4:
|
||||||
version "3.2.4"
|
version "3.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.4.tgz#d8c7892dd4268064e04fffbe7d866207dd24166e"
|
resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.4.tgz#d8c7892dd4268064e04fffbe7d866207dd24166e"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user