inbox-route

This commit is contained in:
Untone 2024-08-02 00:32:52 +03:00
parent a39190e2b1
commit b53f83947c
10 changed files with 207 additions and 161 deletions

View File

@ -20,7 +20,6 @@ bun run typecheck
bun run fix
```
## End-to-End (E2E) тесты
End-to-end тесты написаны с использованием [Playwright](https://playwright.dev/).
@ -47,10 +46,7 @@ End-to-end тесты написаны с использованием [Playwrig
### 🚀 Тесты в режиме CI
Тесты выполняются в рамках GitHub workflow. Мы организуем наши тесты в две основные директории:
- `tests`: Содержит тесты, которые не требуют аутентификации.
- `tests-with-auth`: Содержит тесты, которые взаимодействуют с аутентифицированными частями приложения.
Тесты выполняются в рамках GitHub workflow из папки `tests`
🔧 **Конфигурация:**

View File

@ -25,6 +25,3 @@ generates:
useTypeImports: true
outputPath: './src/graphql/types/core.gen.ts'
# namingConvention: change-case#CamelCase # for generated types
hooks:
afterAllFileWrite:
- prettier --ignore-path .gitignore --write --plugin-search-dir=. src/graphql/schema/*.gen.ts

View File

@ -1,7 +1,6 @@
import { useNavigate } from '@solidjs/router'
import { clsx } from 'clsx'
import { Match, Show, Switch, createEffect, createMemo, createSignal, on } from 'solid-js'
import { useNavigate, useSearchParams } from '@solidjs/router'
import { Button } from '~/components/_shared/Button'
import { CheckButton } from '~/components/_shared/CheckButton'
import { ConditionalWrapper } from '~/components/_shared/ConditionalWrapper'
@ -49,17 +48,13 @@ export const AuthorBadge = (props: Props) => {
)
)
const [, changeSearchParams] = useSearchParams()
const navigate = useNavigate()
const { t, formatDate, lang } = useLocalize()
const initChat = () => {
// eslint-disable-next-line solid/reactivity
requireAuthentication(() => {
navigate('/inbox')
changeSearchParams({
initChat: props.author?.id.toString()
})
props.author?.id && navigate(`/inbox/${props.author?.id}`, { replace: true })
}, 'discussions')
}

View File

@ -1,6 +1,6 @@
import { redirect, useNavigate, useSearchParams } from '@solidjs/router'
import { redirect, useNavigate } from '@solidjs/router'
import { clsx } from 'clsx'
import { For, Show, createEffect, createMemo, createSignal, onMount } from 'solid-js'
import { For, Show, createEffect, createMemo, createSignal, on } from 'solid-js'
import { Button } from '~/components/_shared/Button'
import stylesButton from '~/components/_shared/Button/Button.module.scss'
import { FollowingCounters } from '~/components/_shared/FollowingCounters/FollowingCounters'
@ -34,11 +34,7 @@ export const AuthorCard = (props: Props) => {
const [followsFilter, setFollowsFilter] = createSignal<FollowsFilter>('all')
const [isFollowed, setIsFollowed] = createSignal<boolean>()
const isProfileOwner = createMemo(() => author()?.slug === props.author.slug)
const { follow, unfollow, follows, following } = useFollowing()
onMount(() => {
setAuthorSubs(props.flatFollows || [])
})
const { follow, unfollow, follows, following } = useFollowing() // viewer's followings
createEffect(() => {
if (!(follows && props.author)) return
@ -56,30 +52,22 @@ export const AuthorCard = (props: Props) => {
return props.author.name
})
const [, changeSearchParams] = useSearchParams()
const initChat = () => {
// eslint-disable-next-line solid/reactivity
requireAuthentication(() => {
navigate('/inbox')
changeSearchParams({
initChat: props.author?.id.toString()
})
props.author?.id && navigate(`/inbox/${props.author?.id}`, { replace: true })
}, 'discussions')
}
createEffect(() => {
if (props.flatFollows) {
if (followsFilter() === 'authors') {
setAuthorSubs(props.flatFollows.filter((s) => 'name' in s))
} else if (followsFilter() === 'topics') {
setAuthorSubs(props.flatFollows.filter((s) => 'title' in s))
} else if (followsFilter() === 'communities') {
setAuthorSubs(props.flatFollows.filter((s) => 'title' in s))
} else {
setAuthorSubs(props.flatFollows)
}
}
})
createEffect(
on(followsFilter, (f = 'all') => {
const subs =
f !== 'all'
? follows[f as keyof typeof follows]
: [...(follows.topics || []), ...(follows.authors || [])]
setAuthorSubs(subs || [])
})
)
const handleFollowClick = () => {
requireAuthentication(() => {

View File

@ -57,8 +57,8 @@ const CreateModalContent = (props: Props) => {
const handleCreate = async () => {
try {
const initChat = await createChat(usersId(), chatTitle())
console.debug('[components.Inbox] create chat result:', initChat)
const result = await createChat(usersId(), chatTitle())
console.debug('[components.Inbox] create chat result:', result)
hideModal()
await loadChats()
} catch (error) {

View File

@ -1,6 +1,7 @@
import { useNavigate } from '@solidjs/router'
import { clsx } from 'clsx'
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
import QuotedMessage from '~/components/Inbox/QuotedMessage'
import { Icon } from '~/components/_shared/Icon'
import { InviteMembers } from '~/components/_shared/InviteMembers'
import { Popover } from '~/components/_shared/Popover'
@ -15,6 +16,7 @@ import type {
MutationCreate_MessageArgs
} from '~/graphql/schema/chat.gen'
import type { Author } from '~/graphql/schema/core.gen'
import { getShortDate } from '~/utils/date'
import SimplifiedEditor from '../../Editor/SimplifiedEditor'
import DialogCard from '../../Inbox/DialogCard'
import DialogHeader from '../../Inbox/DialogHeader'
@ -22,28 +24,16 @@ import { Message } from '../../Inbox/Message'
import MessagesFallback from '../../Inbox/MessagesFallback'
import Search from '../../Inbox/Search'
import { Modal } from '../../_shared/Modal'
import { useSearchParams } from '@solidjs/router'
import styles from './Inbox.module.scss'
type InboxSearchParams = {
by?: string
initChat: string
chat: string
}
const userSearch = (array: Author[], keyword: string) => {
return array.filter((value) => new RegExp(keyword.trim(), 'gi').test(value.name || ''))
}
type Props = {
authors: Author[]
isLoaded: boolean
}
export const InboxView = (props: Props) => {
export const InboxView = (props: { authors: Author[]; chat?: Chat }) => {
const { t } = useLocalize()
const { chats, messages, setMessages, loadChats, getMessages, sendMessage, createChat } = useInbox()
const { chats, messages, setMessages, loadChats, getMessages, sendMessage } = useInbox()
const [recipients, setRecipients] = createSignal<Author[]>(props.authors)
const [sortByGroup, setSortByGroup] = createSignal(false)
const [sortByPerToPer, setSortByPerToPer] = createSignal(false)
@ -53,7 +43,6 @@ export const InboxView = (props: Props) => {
const [isScrollToNewVisible, setIsScrollToNewVisible] = createSignal(false)
const { session } = useSession()
const authorId = createMemo<number>(() => session()?.user?.app_data?.profile?.id || 0)
const [searchParams, changeSearchParams] = useSearchParams<InboxSearchParams>()
const { showModal } = useUI()
const handleOpenInviteModal = () => showModal('inviteMembers')
let messagesContainerRef: HTMLDivElement | null
@ -64,12 +53,10 @@ export const InboxView = (props: Props) => {
setRecipients(match)
}
}
const navigate = useNavigate()
const handleOpenChat = async (chat: Chat) => {
setCurrentDialog(chat)
changeSearchParams({
chat: chat.id
})
navigate(`/inbox/${chat.id}`)
try {
const mmm = await getMessages?.(chat.id)
if (mmm) {
@ -98,28 +85,11 @@ export const InboxView = (props: Props) => {
setClear(false)
}
createEffect(async () => {
if (searchParams?.chat) {
const chatToOpen = chats()?.find((chat) => chat.id.toString() === searchParams?.chat)
if (!chatToOpen) return
await handleOpenChat(chatToOpen)
return
}
if (searchParams?.initChat) {
try {
const newChat = await createChat([Number(searchParams?.initChat)], '')
await loadChats()
changeSearchParams({
initChat: undefined,
chat: newChat.chat.id
})
const chatToOpen = chats().find((chat) => chat.id === newChat.chat.id)
await handleOpenChat(chatToOpen as Chat)
} catch (error) {
console.error(error)
}
}
})
createEffect(
on([() => props.chat, currentDialog], ([c, current]) => {
c?.id !== current?.id && handleOpenChat(c as Chat)
})
)
const chatsToShow = () => {
if (!chats()) return
@ -173,9 +143,85 @@ export const InboxView = (props: Props) => {
}
onMount(async () => {
props.chat && setCurrentDialog(props.chat)
await loadChats()
})
const InboxNav = () => (
<div class={clsx(styles.chatList, 'col-md-8')}>
<div class={styles.sidebarHeader}>
<Search placeholder="Поиск" onChange={getQuery} />
<button type="button" onClick={handleOpenInviteModal}>
<Icon name="plus-button" style={{ width: '40px', height: '40px' }} />
</button>
</div>
<Show when={chatsToShow()}>
<ul class="view-switcher">
<li
class={clsx({
'view-switcher__item--selected': !(sortByPerToPer() || sortByGroup())
})}
>
<button
onClick={() => {
setSortByPerToPer(false)
setSortByGroup(false)
}}
>
{t('All')}
</button>
</li>
<li
class={clsx({
'view-switcher__item--selected': sortByPerToPer()
})}
>
<button
onClick={() => {
setSortByPerToPer(true)
setSortByGroup(false)
}}
>
{t('Personal')}
</button>
</li>
<li
class={clsx({
'view-switcher__item--selected': sortByGroup()
})}
>
<button
onClick={() => {
setSortByGroup(true)
setSortByPerToPer(false)
}}
>
{t('Groups')}
</button>
</li>
</ul>
</Show>
<div class={styles.holder}>
<div class={styles.dialogs}>
<For each={chatsToShow()}>
{(chat) => (
<DialogCard
onClick={() => handleOpenChat(chat)}
isOpened={chat.id === currentDialog()?.id}
members={chat?.members as ChatMember[]}
ownId={authorId()}
lastUpdate={chat.updated_at || Date.now()}
counter={chat.unread || 0}
message={chat.messages?.pop()?.body || ''}
/>
)}
</For>
</div>
</div>
</div>
)
return (
<div class={clsx('container', styles.Inbox)}>
<Modal variant="medium" name="inviteMembers">
@ -183,66 +229,7 @@ export const InboxView = (props: Props) => {
</Modal>
{/*<CreateModalContent users={recipients()} />*/}
<div class={clsx('row', styles.row)}>
<div class={clsx(styles.chatList, 'col-md-8')}>
<div class={styles.sidebarHeader}>
<Search placeholder="Поиск" onChange={getQuery} />
<button type="button" onClick={handleOpenInviteModal}>
<Icon name="plus-button" style={{ width: '40px', height: '40px' }} />
</button>
</div>
<Show when={chatsToShow()}>
<ul class="view-switcher">
<li class={clsx({ 'view-switcher__item--selected': !(sortByPerToPer() || sortByGroup()) })}>
<button
onClick={() => {
setSortByPerToPer(false)
setSortByGroup(false)
}}
>
{t('All')}
</button>
</li>
<li class={clsx({ 'view-switcher__item--selected': sortByPerToPer() })}>
<button
onClick={() => {
setSortByPerToPer(true)
setSortByGroup(false)
}}
>
{t('Personal')}
</button>
</li>
<li class={clsx({ 'view-switcher__item--selected': sortByGroup() })}>
<button
onClick={() => {
setSortByGroup(true)
setSortByPerToPer(false)
}}
>
{t('Groups')}
</button>
</li>
</ul>
</Show>
<div class={styles.holder}>
<div class={styles.dialogs}>
<For each={chatsToShow()}>
{(chat) => (
<DialogCard
onClick={() => handleOpenChat(chat)}
isOpened={chat.id === currentDialog()?.id}
members={chat?.members as ChatMember[]}
ownId={authorId()}
lastUpdate={chat.updated_at || Date.now()}
counter={chat.unread || 0}
message={chat.messages?.pop()?.body || ''}
/>
)}
</For>
</div>
</div>
</div>
<InboxNav />
<div class={clsx('col-md-16', styles.conversation)}>
<Show
@ -283,24 +270,26 @@ export const InboxView = (props: Props) => {
/>
)}
</For>
{/*<div class={styles.conversationDate}>*/}
{/* <time>12 сентября</time>*/}
{/*</div>*/}
<Show when={currentDialog()?.created_at}>
<small>
<time>{getShortDate(new Date(currentDialog()?.created_at || 0))}</time>
</small>
</Show>
</div>
</div>
<div class={styles.messageForm}>
<Show when={messageToReply()}>
<p>FIXME: messageToReply</p>
{/*<QuotedMessage*/}
{/* variant="reply"*/}
{/* author={*/}
{/* currentDialog().members.find((member) => member.id === Number(messageToReply().author))*/}
{/* .name*/}
{/* }*/}
{/* body={messageToReply().body}*/}
{/* cancel={() => setMessageToReply(null)}*/}
{/*/>*/}
<Show when={messageToReply()?.body}>
<QuotedMessage
variant="reply"
author={
currentDialog()?.members?.find(
(member) => member?.id === Number(messageToReply()?.created_by)
)?.name
}
body={messageToReply()?.body || ''}
cancel={() => setMessageToReply(null)}
/>
</Show>
<div class={styles.wrapper}>
<SimplifiedEditor

View File

@ -98,8 +98,8 @@ export const InviteMembers = (props: Props) => {
const handleCreate = async () => {
try {
const initChat = await createChat(collectionToInvite(), 'chat Title')
console.debug('[components.Inbox] create chat result:', initChat)
const result = await createChat(collectionToInvite(), 'chat Title')
console.debug('[components.Inbox] create chat result:', result)
hideModal()
await loadChats()
} catch (error) {

View File

@ -15,7 +15,7 @@ const fetchAuthorsWithStat = async (offset = 0, order?: string) => {
return await authorsFetcher()
}
const fetchAllAuthors = async () => {
export const fetchAllAuthors = async () => {
const authorsAllFetcher = loadAuthorsAll()
return await authorsAllFetcher()
}

View File

@ -0,0 +1,28 @@
import { RouteDefinition, RouteSectionProps, createAsync } from '@solidjs/router'
import { InboxView } from '~/components/Views/Inbox/Inbox'
import { PageLayout } from '~/components/_shared/PageLayout'
import { ShowOnlyOnClient } from '~/components/_shared/ShowOnlyOnClient'
import { useLocalize } from '~/context/localize'
import { Author } from '~/graphql/schema/core.gen'
import { fetchAllAuthors } from '../author/(all-authors)'
export const route = {
load: async () => {
return {
authors: await fetchAllAuthors()
}
}
} satisfies RouteDefinition
export const InboxPage = (props: RouteSectionProps<{ authors: Author[] }>) => {
const { t } = useLocalize()
const authors = createAsync(async () => props.data.authors || (await fetchAllAuthors()))
return (
<PageLayout hideFooter={true} title={t('Inbox')}>
<ShowOnlyOnClient>
<InboxView authors={authors() || []} />
</ShowOnlyOnClient>
</PageLayout>
)
}

View File

@ -0,0 +1,53 @@
import { RouteDefinition, RouteSectionProps, createAsync, useParams } from '@solidjs/router'
import { createSignal, onMount } from 'solid-js'
import { InboxView } from '~/components/Views/Inbox/Inbox'
import { PageLayout } from '~/components/_shared/PageLayout'
import { ShowOnlyOnClient } from '~/components/_shared/ShowOnlyOnClient'
import { useInbox } from '~/context/inbox'
import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session'
import { Chat } from '~/graphql/schema/chat.gen'
import { Author } from '~/graphql/schema/core.gen'
import { fetchAllAuthors } from '../author/(all-authors)'
export const route = {
load: async () => {
return {
authors: await fetchAllAuthors()
}
}
} satisfies RouteDefinition
export const ChatPage = (props: RouteSectionProps<{ authors: Author[] }>) => {
const { t } = useLocalize()
const params = useParams()
const { createChat, chats } = useInbox()
const [chat, setChat] = createSignal<Chat>()
const { session } = useSession()
const authors = createAsync(async () => props.data.authors || (await fetchAllAuthors()))
onMount(async () => {
if (params.id.includes('-')) {
// real chat id contains -
setChat((_) => chats().find((x: Chat) => x.id === params.id))
} else {
try {
// handle if params.id is an author's id
const me = session()?.user?.app_data?.profile.id as number
const author = Number.parseInt(params.chat)
const result = await createChat([author, me], '')
// result.chat.id && redirect(`/inbox/${result.chat.id}`)
result.chat && setChat(result.chat)
} catch (e) {
console.warn(e)
}
}
})
return (
<PageLayout hideFooter={true} title={t('Inbox')}>
<ShowOnlyOnClient>
<InboxView authors={authors() || []} chat={chat() as Chat} />
</ShowOnlyOnClient>
</PageLayout>
)
}