Merge remote-tracking branch 'origin/prepare-inbox' into testing

This commit is contained in:
tonyrewin 2022-11-27 14:02:16 +03:00
commit d68cb92462
20 changed files with 296 additions and 75 deletions

3
public/icons/cross.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.94025 5L0.227936 1.28769L1.2886 0.227029L5.00091 3.93934L8.71322 0.227029L9.77388 1.28769L6.06157 5L9.77388 8.71231L8.71322 9.77297L5.00091 6.06066L1.2886 9.77297L0.227936 8.71231L3.94025 5Z" fill="#D00820"/>
</svg>

After

Width:  |  Height:  |  Size: 364 B

View File

@ -0,0 +1,4 @@
<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="M19 19V12H21V19H28V21H21L21 28H19L19 21H12V19H19Z" fill="#404040"/>
<rect x="1" y="1" width="38" height="38" rx="11" stroke="#404040" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 305 B

3
public/icons/plus.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.25098 5.25V0L6.75098 0V5.25L12.001 5.25V6.75L6.75098 6.75L6.75098 12H5.25098L5.25098 6.75H0.000976563L0.000976563 5.25H5.25098Z" fill="#141414"/>
</svg>

After

Width:  |  Height:  |  Size: 301 B

View File

@ -0,0 +1,9 @@
.CreateModalContent {
padding: 24px;
.footer {
padding-top: 12px;
display: flex;
justify-content: space-around;
gap: 12px;
}
}

View File

@ -0,0 +1,112 @@
import { createSignal, For, createEffect } from 'solid-js'
import styles from './CreateModalContent.module.scss'
import { t } from '../../utils/intl'
import InviteUser from './InviteUser'
import type { Author } from '../../graphql/types.gen'
import { hideModal } from '../../stores/ui'
import { useInbox } from '../../context/inbox'
type inviteUser = Author & { selected: boolean }
type query =
| {
theme: string
members: string[]
}
| undefined
type Props = {
users: Author[]
}
const CreateModalContent = (props: Props) => {
const inviteUsers: inviteUser[] = props.users.map((user) => ({ ...user, selected: false }))
const [theme, setTheme] = createSignal<string>('')
const [slugs, setSlugs] = createSignal<string[]>([])
const [collectionToInvite, setCollectionToInvite] = createSignal<inviteUser[]>(inviteUsers)
let textInput: HTMLInputElement
const reset = () => {
setTheme('')
setSlugs([])
hideModal()
}
createEffect(() => {
setSlugs(() => {
return collectionToInvite()
.filter((user) => {
return user.selected === true
})
.map((user) => {
return user['slug']
})
})
if (slugs().length > 2 && theme().length === 0) {
setTheme(t('group_chat'))
}
})
const handleSetTheme = () => {
setTheme(textInput.value.length > 0 && textInput.value)
}
const handleClick = (user) => {
setCollectionToInvite((userCollection) => {
return userCollection.map((clickedUser) =>
user.slug === clickedUser.slug ? { ...clickedUser, selected: !clickedUser.selected } : clickedUser
)
})
}
const { chatEntities, actions } = useInbox()
console.log('!!! chatEntities:', chatEntities)
const handleCreate = async () => {
try {
const initChat = await actions.createChat(slugs(), theme())
console.debug('[initChat]', initChat)
} catch (error) {
console.error(error)
}
}
return (
<div class={styles.CreateModalContent}>
<h4>{t('create_chat')}</h4>
{slugs().length > 2 && (
<input
ref={textInput}
onInput={handleSetTheme}
type="text"
required={true}
class="form-control form-control-lg fs-3"
placeholder={t('discourse_theme')}
/>
)}
<div class="invite-recipients" style={{ height: '400px', overflow: 'auto' }}>
<For each={collectionToInvite()}>
{(author) => (
<InviteUser onClick={() => handleClick(author)} author={author} selected={author.selected} />
)}
</For>
</div>
<div class={styles.footer}>
<button type="button" class="btn btn-lg fs-3 btn-outline-danger" onClick={reset}>
{t('cancel')}
</button>
<button
type="button"
class="btn btn-lg fs-3 btn-outline-primary"
onClick={handleCreate}
disabled={slugs().length === 0}
>
{slugs().length > 2 ? t('create_group') : t('create_chat')}
</button>
</div>
</div>
)
}
export default CreateModalContent

View File

@ -1,6 +1,6 @@
import styles from './DialogCard.module.scss'
import DialogAvatar from './DialogAvatar'
import type { Author } from '../../graphql/types.gen'
import type { Author, User } from '../../graphql/types.gen'
import { apiClient } from '../../utils/apiClient'
import { t } from '../../utils/intl'
import { useInbox } from '../../context/inbox'
@ -9,29 +9,21 @@ type DialogProps = {
online?: boolean
message?: string
counter?: number
author?: Author
ownSlug: Author['slug']
users: User[]
ownSlug: User['slug']
}
const DialogCard = (props: DialogProps) => {
const { chatEntities, actions } = useInbox()
const handleOpenChat = async () => {
try {
const initChat = await actions.createChat([props.author.slug, props.ownSlug])
console.debug('[initChat]', initChat)
} catch (error) {
console.error(error)
}
}
// @ts-ignore
const participants = props.users.filter((user) => user !== props.ownSlug)
console.log('!!! participants:', participants)
// @ts-ignore
return (
//DialogCardView - подумать
<div class={styles.DialogCard} onClick={handleOpenChat}>
<div class={styles.avatar}>
<DialogAvatar name={props.author.name} url={props.author.userpic} online={props.online} />
</div>
<div class={styles.DialogCard}>
<div class={styles.avatar}>{/*<DialogAvatar name={participants[0]} online={props.online} />*/}</div>
<div class={styles.row}>
<div class={styles.name}>{props.author.name}</div>
{/*<div class={styles.name}>{participants[0]}</div>*/}
<div class={styles.message}>
Указать предпочтительные языки для результатов поиска можно в разделе
</div>

View File

@ -0,0 +1,35 @@
.InviteUser {
display: flex;
align-items: center;
justify-content: space-between;
flex-basis: 0;
flex-grow: 1;
min-width: 0;
padding: 12px;
gap: 10px;
cursor: pointer;
transition: all 0.3s ease-in-out;
&:hover {
background: rgba(#f7f7f7, 0.65);
}
.name {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
font-size: 14px;
}
.action {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: #f7f7f7;
border-radius: 6px;
}
}

View File

@ -0,0 +1,22 @@
import styles from './InviteUser.module.scss'
import DialogAvatar from './DialogAvatar'
import type { Author } from '../../graphql/types.gen'
import { Icon } from '../_shared/Icon'
type DialogProps = {
author: Author
selected: boolean
onClick: () => void
}
const InviteUser = (props: DialogProps) => {
return (
<div class={styles.InviteUser} onClick={props.onClick}>
<DialogAvatar name={props.author.name} url={props.author.userpic} />
<div class={styles.name}>{props.author.name}</div>
<div class={styles.action}>{props.selected ? <Icon name="cross" /> : <Icon name="plus" />}</div>
</div>
)
}
export default InviteUser

View File

@ -8,7 +8,7 @@
input {
display: block;
height: 40px;
height: 36px;
border: none;
box-shadow: none;
padding: 10px 36px 10px 12px;
@ -38,7 +38,7 @@
position: absolute;
width: 16px;
height: 16px;
top: 12px;
top: 10px;
right: 12px;
opacity: 0.5;
}

View File

@ -84,7 +84,7 @@ export const Header = (props: Props) => {
[styles.headerWithTitle]: Boolean(props.title)
}}
>
<Modal name="auth">
<Modal variant="wide" name="auth">
<AuthModal />
</Modal>

View File

@ -1,4 +1,4 @@
.modalwrap {
.backdrop {
align-items: center;
background: rgb(20 20 20 / 70%);
display: flex;
@ -10,9 +10,16 @@
position: fixed;
top: 0;
width: 100%;
z-index: 10;
z-index: 100;
}
.close-control {
.modal {
background: #fff;
max-width: 1000px;
position: relative;
width: 80%;
.close {
position: absolute;
top: 1em;
cursor: pointer;
@ -41,19 +48,16 @@
// top: 11em;
}
}
}
.modalwrap__inner {
background: #fff;
max-width: 1000px;
position: relative;
width: 80%;
}
.modalwrap__content {
padding: $container-padding-x;
&.narrow {
max-width: 460px;
width: 50%;
@media (min-width: 800px) and (max-width: 991px) {
padding: 10rem 6rem;
width: 80%;
}
.close {
right: 12px;
top: 12px;
}
}
}

View File

@ -1,22 +1,24 @@
import { createEffect, createSignal, Show } from 'solid-js'
import type { JSX } from 'solid-js'
import { getLogger } from '../../utils/logger'
import './Modal.scss'
import { hideModal, useModalStore } from '../../stores/ui'
import { useEscKeyDownHandler } from '../../utils/useEscKeyDownHandler'
import { clsx } from 'clsx'
import styles from './Modal.module.scss'
const log = getLogger('modal')
interface ModalProps {
name: string
variant: 'narrow' | 'wide'
children: JSX.Element
}
export const Modal = (props: ModalProps) => {
const { modal } = useModalStore()
const wrapClick = (event: { target: Element }) => {
if (event.target.classList.contains('modalwrap')) hideModal()
const backdropClick = (event: Event) => {
hideModal()
}
useEscKeyDownHandler(() => hideModal())
@ -30,10 +32,16 @@ export const Modal = (props: ModalProps) => {
return (
<Show when={visible()}>
<div class="modalwrap" onClick={wrapClick}>
<div class="modalwrap__inner">
<div class={styles.backdrop} onClick={backdropClick}>
<div
class={clsx(styles.modal, {
[styles.wide]: props.variant === 'wide',
[styles.narrow]: props.variant === 'narrow'
})}
onClick={(event) => event.stopPropagation()}
>
{props.children}
<div class="close-control" onClick={hideModal}>
<div class={styles.close} onClick={hideModal}>
<svg width="16" height="18" viewBox="0 0 16 18" xmlns="http://www.w3.org/2000/svg">
<path
d="M7.99987 7.52552L14.1871 0.92334L15.9548 2.80968L9.76764 9.41185L15.9548 16.014L14.1871 17.9004L7.99987 11.2982L1.81269 17.9004L0.0449219 16.014L6.23211 9.41185L0.0449225 2.80968L1.81269 0.92334L7.99987 7.52552Z"

View File

@ -15,10 +15,10 @@ export const ManifestPage = () => {
return (
<PageWrap>
<Modal name="feedback">
<Modal variant="wide" name="feedback">
<Feedback />
</Modal>
<Modal name="subscribe">
<Modal variant="wide" name="subscribe">
<Subscribe />
</Modal>
<article class="wide-container container--static-page">

View File

@ -1,5 +1,5 @@
import { For, createSignal, Show, onMount, createEffect, createMemo } from 'solid-js'
import type { Author } from '../../graphql/types.gen'
import type { Author, Chat } from '../../graphql/types.gen'
import { AuthorCard } from '../Author/Card'
import { Icon } from '../_shared/Icon'
import { Loading } from '../Loading'
@ -12,6 +12,10 @@ import { loadRecipients, loadChats } from '../../stores/inbox'
import { t } from '../../utils/intl'
import '../../styles/Inbox.scss'
import { useInbox } from '../../context/inbox'
import { Modal } from '../Nav/Modal'
import { showModal } from '../../stores/ui'
import InviteUser from '../Inbox/InviteUser'
import CreateModalContent from '../Inbox/CreateModalContent'
const OWNER_ID = '501'
const client = createClient({
@ -54,18 +58,10 @@ const postMessage = async (msg: string) => {
return response.data.createComment
}
const handleGetChats = async () => {
try {
const response = await loadChats()
console.log('!!! handleGetChats:', response)
} catch (error) {
console.log(error)
}
}
export const InboxView = () => {
const [messages, setMessages] = createSignal([])
const [recipients, setRecipients] = createSignal<Author[]>([])
const [chats, setChats] = createSignal<Chat[]>([])
const [cashedRecipients, setCashedRecipients] = createSignal<Author[]>([])
const [postMessageText, setPostMessageText] = createSignal('')
const [loading, setLoading] = createSignal<boolean>(false)
@ -112,6 +108,13 @@ export const InboxView = () => {
} catch (error) {
console.log(error)
}
try {
const response = await loadChats()
setChats(response as unknown as Chat[])
} catch (error) {
console.log(error)
}
})
const handleSubmit = async () => {
@ -125,36 +128,46 @@ export const InboxView = () => {
}
}
let formParent // autoresize ghost element
let textareaParent // textarea autoresize ghost element
const handleChangeMessage = (event) => {
setPostMessageText(event.target.value)
}
createEffect(() => {
formParent.dataset.replicatedValue = postMessageText()
textareaParent.dataset.replicatedValue = postMessageText()
})
// FIXME: прописать типы
// const { chatEntitieies, actions: { createCaht }} = useInbox()
// const { actions: { createCaht }} = useInbox()
const handleOpenInviteModal = (event: Event) => {
event.preventDefault()
showModal('inviteToChat')
}
return (
<div class="messages container">
<Modal variant="narrow" name="inviteToChat">
<CreateModalContent users={recipients()} />
</Modal>
<div class="row">
<div class="chat-list col-md-4">
<div class="sidebar-header">
<Search placeholder="Поиск" onChange={getQuery} />
<div onClick={handleOpenInviteModal}>
<Icon name="plus-button" style={{ width: '40px', height: '40px' }} />
</div>
</div>
<div class="chat-list__types">
<ul>
<li>
<strong>{t('All')}</strong>
</li>
<li onClick={handleGetChats}>{t('Conversations')}</li>
<li>{t('Personal')}</li>
<li>{t('Groups')}</li>
</ul>
</div>
<div class="holder">
<div class="dialogs">
<For each={recipients()}>
{(author) => <DialogCard ownSlug={currentSlug()} author={author} online={true} />}
<For each={chats()}>
{(chat) => <DialogCard users={chat.users} ownSlug={currentSlug()} />}
</For>
</div>
</div>
@ -185,7 +198,7 @@ export const InboxView = () => {
<div class="message-form">
<div class="wrapper">
<div class="grow-wrap" ref={formParent}>
<div class="grow-wrap" ref={textareaParent}>
<textarea
value={postMessageText()}
rows={1}

View File

@ -7,7 +7,7 @@ import { createStore } from 'solid-js/store'
type InboxContextType = {
chatEntities: { [chatId: string]: Message[] }
actions: {
createChat: (members: string[], title?: string) => Promise<void>
createChat: (members: string[], title: string) => Promise<void>
}
}
@ -20,8 +20,8 @@ export function useInbox() {
export const InboxProvider = (props: { children: JSX.Element }) => {
const [chatEntities, setChatEntities] = createStore({})
const createChat = async (members: string[]) => {
const chat = await apiClient.createChat({ members })
const createChat = async (members: string[], title: string) => {
const chat = await apiClient.createChat({ members, title })
setChatEntities((s) => {
s[chat.id] = chat
})
@ -31,6 +31,7 @@ export const InboxProvider = (props: { children: JSX.Element }) => {
const actions = {
createChat
}
const value: InboxContextType = { chatEntities, actions }
return <InboxContext.Provider value={value}>{props.children}</InboxContext.Provider>
}

View File

@ -181,7 +181,12 @@
"zine": "журнал",
"shout": "пост",
"discussion": "дискурс",
"Conversations": "Переписки",
"Personal": "Личные",
"Groups": "Группы",
"All": "Все"
"All": "Все",
"create_chat": "Создать чат",
"create_group": "Создать группу",
"discourse_theme": "Тема дискурса",
"cancel": "Отмена",
"group_chat": "Общий чат"
}

View File

@ -4,7 +4,7 @@ import type { AuthModalSearchParams, ConfirmEmailSearchParams } from '../compone
import type { RootSearchParams } from '../components/types'
export const [locale, setLocale] = createSignal('ru')
export type ModalType = 'auth' | 'subscribe' | 'feedback' | 'thank' | 'donate'
export type ModalType = 'auth' | 'subscribe' | 'feedback' | 'thank' | 'donate' | 'inviteToChat'
type WarnKind = 'error' | 'warn' | 'info'
export interface Warning {
@ -18,7 +18,8 @@ export const MODALS: Record<ModalType, ModalType> = {
subscribe: 'subscribe',
feedback: 'feedback',
thank: 'thank',
donate: 'donate'
donate: 'donate',
inviteToChat: 'inviteToChat'
}
const [modal, setModal] = createSignal<ModalType | null>(null)

View File

@ -17,7 +17,7 @@ main {
flex: 1;
flex-direction: column;
position: fixed;
z-index: 9;
z-index: 900;
.row {
flex: 1;
@ -41,6 +41,12 @@ main {
$fade-height: 10px;
.sidebar-header {
display: flex;
align-items: center;
gap: 10px;
}
.holder {
overflow: hidden;
flex: 1;
@ -105,11 +111,13 @@ main {
li {
margin-right: 1em;
color: #696969;
}
strong {
border-bottom: 3px solid;
font-weight: normal;
color: #000;
}
}

View File

@ -4,6 +4,8 @@
@import 'bootstrap/scss/containers';
@import 'bootstrap/scss/grid';
@import 'bootstrap/scss/bootstrap-utilities';
@import 'bootstrap/scss/forms';
@import 'bootstrap/scss/buttons';
:root {
--background-color: #fff;

View File

@ -272,8 +272,7 @@ export const apiClient = {
// inbox
getChats: async (options: QueryLoadChatsArgs) => {
const resp = await privateGraphQLClient.query(myChats, options).toPromise()
console.debug('[loadChats]', resp)
return resp.data.loadChats
return resp.data.loadChats.chats
},
createChat: async (options: MutationCreateChatArgs) => {