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

View File

@ -25,6 +25,3 @@ generates:
useTypeImports: true useTypeImports: true
outputPath: './src/graphql/types/core.gen.ts' outputPath: './src/graphql/types/core.gen.ts'
# namingConvention: change-case#CamelCase # for generated types # 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 { clsx } from 'clsx'
import { Match, Show, Switch, createEffect, createMemo, createSignal, on } from 'solid-js' import { Match, Show, Switch, createEffect, createMemo, createSignal, on } from 'solid-js'
import { useNavigate, useSearchParams } from '@solidjs/router'
import { Button } from '~/components/_shared/Button' import { Button } from '~/components/_shared/Button'
import { CheckButton } from '~/components/_shared/CheckButton' import { CheckButton } from '~/components/_shared/CheckButton'
import { ConditionalWrapper } from '~/components/_shared/ConditionalWrapper' import { ConditionalWrapper } from '~/components/_shared/ConditionalWrapper'
@ -49,17 +48,13 @@ export const AuthorBadge = (props: Props) => {
) )
) )
const [, changeSearchParams] = useSearchParams()
const navigate = useNavigate() const navigate = useNavigate()
const { t, formatDate, lang } = useLocalize() const { t, formatDate, lang } = useLocalize()
const initChat = () => { const initChat = () => {
// eslint-disable-next-line solid/reactivity // eslint-disable-next-line solid/reactivity
requireAuthentication(() => { requireAuthentication(() => {
navigate('/inbox') props.author?.id && navigate(`/inbox/${props.author?.id}`, { replace: true })
changeSearchParams({
initChat: props.author?.id.toString()
})
}, 'discussions') }, '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 { 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 { Button } from '~/components/_shared/Button'
import stylesButton from '~/components/_shared/Button/Button.module.scss' import stylesButton from '~/components/_shared/Button/Button.module.scss'
import { FollowingCounters } from '~/components/_shared/FollowingCounters/FollowingCounters' import { FollowingCounters } from '~/components/_shared/FollowingCounters/FollowingCounters'
@ -34,11 +34,7 @@ export const AuthorCard = (props: Props) => {
const [followsFilter, setFollowsFilter] = createSignal<FollowsFilter>('all') const [followsFilter, setFollowsFilter] = createSignal<FollowsFilter>('all')
const [isFollowed, setIsFollowed] = createSignal<boolean>() const [isFollowed, setIsFollowed] = createSignal<boolean>()
const isProfileOwner = createMemo(() => author()?.slug === props.author.slug) const isProfileOwner = createMemo(() => author()?.slug === props.author.slug)
const { follow, unfollow, follows, following } = useFollowing() const { follow, unfollow, follows, following } = useFollowing() // viewer's followings
onMount(() => {
setAuthorSubs(props.flatFollows || [])
})
createEffect(() => { createEffect(() => {
if (!(follows && props.author)) return if (!(follows && props.author)) return
@ -56,30 +52,22 @@ export const AuthorCard = (props: Props) => {
return props.author.name return props.author.name
}) })
const [, changeSearchParams] = useSearchParams()
const initChat = () => { const initChat = () => {
// eslint-disable-next-line solid/reactivity // eslint-disable-next-line solid/reactivity
requireAuthentication(() => { requireAuthentication(() => {
navigate('/inbox') props.author?.id && navigate(`/inbox/${props.author?.id}`, { replace: true })
changeSearchParams({
initChat: props.author?.id.toString()
})
}, 'discussions') }, 'discussions')
} }
createEffect(() => { createEffect(
if (props.flatFollows) { on(followsFilter, (f = 'all') => {
if (followsFilter() === 'authors') { const subs =
setAuthorSubs(props.flatFollows.filter((s) => 'name' in s)) f !== 'all'
} else if (followsFilter() === 'topics') { ? follows[f as keyof typeof follows]
setAuthorSubs(props.flatFollows.filter((s) => 'title' in s)) : [...(follows.topics || []), ...(follows.authors || [])]
} else if (followsFilter() === 'communities') { setAuthorSubs(subs || [])
setAuthorSubs(props.flatFollows.filter((s) => 'title' in s))
} else {
setAuthorSubs(props.flatFollows)
}
}
}) })
)
const handleFollowClick = () => { const handleFollowClick = () => {
requireAuthentication(() => { requireAuthentication(() => {

View File

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

View File

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

View File

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

View File

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