inbox-route
This commit is contained in:
parent
a39190e2b1
commit
b53f83947c
|
@ -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`
|
||||
|
||||
🔧 **Конфигурация:**
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
28
src/routes/inbox/(chats).tsx
Normal file
28
src/routes/inbox/(chats).tsx
Normal 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>
|
||||
)
|
||||
}
|
53
src/routes/inbox/[chat].tsx
Normal file
53
src/routes/inbox/[chat].tsx
Normal 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>
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue
Block a user