Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into hotfix/expo

This commit is contained in:
kvakazyambra 2024-05-19 01:04:49 +03:00
commit e4f7675606
16 changed files with 98 additions and 77 deletions

View File

@ -418,6 +418,7 @@
"Username": "Username", "Username": "Username",
"Userpic": "Userpic", "Userpic": "Userpic",
"Users": "Users", "Users": "Users",
"User was not found": "User was not found",
"Video format not supported": "Video format not supported", "Video format not supported": "Video format not supported",
"Video": "Video", "Video": "Video",
"Views": "Views", "Views": "Views",
@ -541,4 +542,4 @@
"Incorrect old password": "Incorrect old password", "Incorrect old password": "Incorrect old password",
"Repeat new password": "Repeat new password", "Repeat new password": "Repeat new password",
"Incorrect new password confirm": "Incorrect new password confirm" "Incorrect new password confirm": "Incorrect new password confirm"
} }

View File

@ -550,6 +550,7 @@
"topicKeywords": "{topic}, Discours.io, статьи, журналистика, исследования", "topicKeywords": "{topic}, Discours.io, статьи, журналистика, исследования",
"topics": "темы", "topics": "темы",
"user already exist": "пользователь уже существует", "user already exist": "пользователь уже существует",
"User was not found": "Пользователь не найден",
"verified": "уже подтверждён", "verified": "уже подтверждён",
"video": "видео", "video": "видео",
"view": "просмотр", "view": "просмотр",
@ -568,4 +569,4 @@
"Incorrect old password": "Старый пароль не верен", "Incorrect old password": "Старый пароль не верен",
"Repeat new password": "Повторите новый пароль", "Repeat new password": "Повторите новый пароль",
"Incorrect new password confirm": "Неверное подтверждение нового пароля" "Incorrect new password confirm": "Неверное подтверждение нового пароля"
} }

View File

@ -538,7 +538,7 @@ export const FullArticle = (props: Props) => {
{(triggerRef: (el) => void) => ( {(triggerRef: (el) => void) => (
<div class={styles.shoutStatsItem} ref={triggerRef}> <div class={styles.shoutStatsItem} ref={triggerRef}>
<a <a
href={getPagePath(router, 'edit', { shoutId: props.article.id.toString() })} href={getPagePath(router, 'edit', { shoutId: props.article?.id.toString() })}
class={styles.shoutStatsItemInner} class={styles.shoutStatsItemInner}
> >
<Icon name="pencil-outline" class={styles.icon} /> <Icon name="pencil-outline" class={styles.icon} />

View File

@ -54,7 +54,7 @@ export const AuthorBadge = (props: Props) => {
requireAuthentication(() => { requireAuthentication(() => {
openPage(router, 'inbox') openPage(router, 'inbox')
changeSearchParams({ changeSearchParams({
initChat: props.author.id.toString(), initChat: props.author?.id.toString(),
}) })
}, 'discussions') }, 'discussions')
} }

View File

@ -65,7 +65,7 @@ export const AuthorCard = (props: Props) => {
requireAuthentication(() => { requireAuthentication(() => {
openPage(router, 'inbox') openPage(router, 'inbox')
changeSearchParams({ changeSearchParams({
initChat: props.author.id.toString(), initChat: props.author?.id.toString(),
}) })
}, 'discussions') }, 'discussions')
} }

View File

@ -60,7 +60,7 @@ export const Draft = (props: Props) => {
<div class={styles.actions}> <div class={styles.actions}>
<a <a
class={styles.actionItem} class={styles.actionItem}
href={getPagePath(router, 'edit', { shoutId: props.shout.id.toString() })} href={getPagePath(router, 'edit', { shoutId: props.shout?.id.toString() })}
> >
{t('Edit')} {t('Edit')}
</a> </a>

View File

@ -329,7 +329,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
<Popover content={t('Edit')} disabled={isActionPopupActive()}> <Popover content={t('Edit')} disabled={isActionPopupActive()}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el) => void) => (
<div class={styles.shoutCardDetailsItem} ref={triggerRef}> <div class={styles.shoutCardDetailsItem} ref={triggerRef}>
<a href={getPagePath(router, 'edit', { shoutId: props.article.id.toString() })}> <a href={getPagePath(router, 'edit', { shoutId: props.article?.id.toString() })}>
<Icon name="pencil-outline" class={clsx(styles.icon, styles.feedControlIcon)} /> <Icon name="pencil-outline" class={clsx(styles.icon, styles.feedControlIcon)} />
<Icon <Icon
name="pencil-outline-hover" name="pencil-outline-hover"

View File

@ -95,20 +95,15 @@ export const LoginForm = () => {
try { try {
const { errors } = await signIn({ email: email(), password: password() }) const { errors } = await signIn({ email: email(), password: password() })
console.error('[signIn errors]', errors)
if (errors?.length > 0) { if (errors?.length > 0) {
if ( console.error('[signIn errors]', errors)
errors.some( if (errors.some((error) => error.message.includes('user has not signed up email & password'))) {
(error) =>
error.message.includes('bad user credentials') || error.message.includes('user not found'),
)
) {
setValidationErrors((prev) => ({ setValidationErrors((prev) => ({
...prev, ...prev,
password: t('Something went wrong, check email and password'), password: t('Something went wrong, check email and password'),
})) }))
} else if (errors.some((error) => error.message.includes('user not found'))) { } else if (errors.some((error) => error.message.includes('user not found'))) {
setSubmitError('Пользователь не найден') setSubmitError(t('User was not found'))
} else if (errors.some((error) => error.message.includes('email not verified'))) { } else if (errors.some((error) => error.message.includes('email not verified'))) {
setSubmitError( setSubmitError(
<div class={styles.info}> <div class={styles.info}>

View File

@ -103,7 +103,13 @@ export const HeaderAuth = (props: Props) => {
<div class={clsx('col-auto col-lg-7', styles.usernav)}> <div class={clsx('col-auto col-lg-7', styles.usernav)}>
<div class={styles.userControl}> <div class={styles.userControl}>
<Show when={isCreatePostButtonVisible() && session()?.access_token}> <Show when={isCreatePostButtonVisible() && session()?.access_token}>
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}> <div
class={clsx(
styles.userControlItem,
styles.userControlItemVerbose,
styles.userControlItemCreate,
)}
>
<a href={getPagePath(router, 'create')}> <a href={getPagePath(router, 'create')}>
<span class={styles.textLabel}>{t('Create post')}</span> <span class={styles.textLabel}>{t('Create post')}</span>
<Icon name="pencil-outline" class={styles.icon} /> <Icon name="pencil-outline" class={styles.icon} />
@ -210,11 +216,17 @@ export const HeaderAuth = (props: Props) => {
</Show> </Show>
<Show when={isCreatePostButtonVisible() && !session()?.access_token}> <Show when={isCreatePostButtonVisible() && !session()?.access_token}>
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}> <div
class={clsx(
styles.userControlItem,
styles.userControlItemVerbose,
styles.userControlItemCreate,
)}
>
<a href={getPagePath(router, 'create')}> <a href={getPagePath(router, 'create')}>
<span class={styles.textLabel}>{t('Create post')}</span> <span class={styles.textLabel}>{t('Create post')}</span>
<Icon name="pencil" class={styles.icon} /> <Icon name="pencil-outline" class={styles.icon} />
<Icon name="pencil" class={clsx(styles.icon, styles.iconHover)} /> <Icon name="pencil-outline-hover" class={clsx(styles.icon, styles.iconHover)} />
</a> </a>
</div> </div>
</Show> </Show>
@ -227,7 +239,7 @@ export const HeaderAuth = (props: Props) => {
<a href="?m=auth&mode=login"> <a href="?m=auth&mode=login">
<span class={styles.textLabel}>{t('Enter')}</span> <span class={styles.textLabel}>{t('Enter')}</span>
<Icon name="key" class={styles.icon} /> <Icon name="key" class={styles.icon} />
{/*<Icon name="user-default" class={clsx(styles.icon, styles.iconHover)} />*/} <Icon name="key" class={clsx(styles.icon, styles.iconHover)} />
</a> </a>
</div> </div>
</Show> </Show>

View File

@ -45,7 +45,6 @@
.info { .info {
@include font-size(1.4rem); @include font-size(1.4rem);
border: none; border: none;
// display: flex; // display: flex;
@ -63,13 +62,11 @@
.title { .title {
@include font-size(2.2rem); @include font-size(2.2rem);
font-weight: bold; font-weight: bold;
} }
.description { .description {
@include font-size(1.6rem); @include font-size(1.6rem);
line-height: 1.4; line-height: 1.4;
margin: 0.8rem 0; margin: 0.8rem 0;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
@ -107,7 +104,6 @@
.title { .title {
@include font-size(1.4rem); @include font-size(1.4rem);
font-weight: 500; font-weight: 500;
line-height: 1em; line-height: 1em;
color: var(--blue-500); color: var(--blue-500);
@ -116,9 +112,7 @@
.description { .description {
color: var(--black-400); color: var(--black-400);
@include font-size(1.2rem); @include font-size(1.2rem);
font-weight: 500; font-weight: 500;
margin: 0; margin: 0;
} }

View File

@ -48,7 +48,7 @@ export const TopicBadge = (props: Props) => {
lang() === 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title lang() === 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title
return ( return (
<div class={clsx(styles.TopicBadge, props.subscriptionsMode)}> <div class={clsx(styles.TopicBadge, { [styles.TopicBadgeSubscriptionsMode]: props.subscriptionsMode })}>
<div class={styles.content}> <div class={styles.content}>
<div class={styles.basicInfo}> <div class={styles.basicInfo}>
<Show when={props.subscriptionsMode}> <Show when={props.subscriptionsMode}>

View File

@ -37,7 +37,6 @@ export const ArticleCardSwiper = (props: Props) => {
[styles.Swiper]: props.slides.length > 1, [styles.Swiper]: props.slides.length > 1,
[styles.articleMode]: true, [styles.articleMode]: true,
[styles.ArticleCardSwiper]: props.slides.length > 1, [styles.ArticleCardSwiper]: props.slides.length > 1,
[styles.unswiped]: props.slides.length === 1,
})} })}
> >
<Show when={props.title}> <Show when={props.title}>

View File

@ -30,53 +30,57 @@ const ConnectContext = createContext<ConnectContextType>()
export const ConnectProvider = (props: { children: JSX.Element }) => { export const ConnectProvider = (props: { children: JSX.Element }) => {
const [messageHandlers, setHandlers] = createSignal<MessageHandler[]>([]) const [messageHandlers, setHandlers] = createSignal<MessageHandler[]>([])
// const [messages, setMessages] = createSignal<Array<SSEMessage>>([]);
const [connected, setConnected] = createSignal(false) const [connected, setConnected] = createSignal(false)
const { session } = useSession() const { session } = useSession()
const [retried, setRetried] = createSignal<number>(0)
const addHandler = (handler: MessageHandler) => { const addHandler = (handler: MessageHandler) => {
setHandlers((hhh) => [...hhh, handler]) setHandlers((hhh) => [...hhh, handler])
} }
const [retried, setRetried] = createSignal<number>(0)
createEffect(async () => { createEffect(async () => {
const token = session()?.access_token const token = session()?.access_token
if (token && !connected()) { if (token && !connected() && retried() <= RECONNECT_TIMES) {
console.info('[context.connect] init SSE connection') console.info('[context.connect] init SSE connection')
await fetchEventSource('https://connect.discours.io', { try {
method: 'GET', await fetchEventSource('https://connect.discours.io', {
headers: { method: 'GET',
'Content-Type': 'application/json', headers: {
Authorization: token, 'Content-Type': 'application/json',
}, Authorization: token,
onmessage(event) { },
const m: SSEMessage = JSON.parse(event.data || '{}') onmessage(event) {
console.log('[context.connect] Received message:', m) const m: SSEMessage = JSON.parse(event.data || '{}')
console.log('[context.connect] Received message:', m)
// Iterate over all registered handlers and call them messageHandlers().forEach((handler) => handler(m))
messageHandlers().forEach((handler) => handler(m)) },
}, onopen: (response) => {
async onopen(response) { console.log('[context.connect] SSE connection opened', response)
console.log('[context.connect] SSE connection opened', response) if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
if (response.ok && response.headers.get('content-type') === EventStreamContentType) { setConnected(true)
setConnected(true) setRetried(0)
} else if (response.status === 401) { return Promise.resolve()
throw new Error('unauthorized') }
} else { return Promise.reject(`SSE: cannot connect to real-time updates, status: ${response.status}`)
setRetried((r) => r + 1) },
throw new Error('Internal Error') onclose() {
} console.log('[context.connect] SSE connection closed by server')
}, setConnected(false)
onclose() { if (retried() < RECONNECT_TIMES) {
console.log('[context.connect] SSE connection closed by server') setRetried((r) => r + 1)
setConnected(false) }
}, },
onerror(err) { onerror(err) {
if (err.message === 'unauthorized' || retried() > RECONNECT_TIMES) { console.error('[context.connect] SSE connection error:', err)
throw err // rethrow to stop the operation setConnected(false)
} if (retried() < RECONNECT_TIMES) {
}, setRetried((r) => r + 1)
}) } else throw Error(err)
},
})
} catch (error) {
console.error('[context.connect] SSE connection failed:', error)
}
} }
}) })

View File

@ -225,9 +225,12 @@ export const SessionProvider = (props: {
const appdata = session()?.user.app_data const appdata = session()?.user.app_data
if (appdata) { if (appdata) {
const { profile } = appdata const { profile } = appdata
setAuthor(profile) if (profile?.id) {
addAuthors([profile]) setAuthor(profile)
if (!profile) loadAuthor() addAuthors([profile])
} else {
setTimeout(loadAuthor, 15)
}
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e)

View File

@ -18,7 +18,7 @@ import styles from '../styles/Create.module.scss'
const handleCreate = async (layout: LayoutType) => { const handleCreate = async (layout: LayoutType) => {
const shout = await apiClient.createArticle({ article: { layout: layout } }) const shout = await apiClient.createArticle({ article: { layout: layout } })
redirectPage(router, 'edit', { redirectPage(router, 'edit', {
shoutId: shout.id.toString(), shoutId: shout?.id.toString(),
}) })
} }

View File

@ -1,4 +1,4 @@
import { Show, Suspense, createEffect, createMemo, createSignal, lazy, on, onMount } from 'solid-js' import { Show, Suspense, createEffect, createMemo, createSignal, lazy, on } from 'solid-js'
import { AuthGuard } from '../components/AuthGuard' import { AuthGuard } from '../components/AuthGuard'
import { Loading } from '../components/_shared/Loading' import { Loading } from '../components/_shared/Loading'
@ -7,7 +7,7 @@ import { useLocalize } from '../context/localize'
import { useSession } from '../context/session' import { useSession } from '../context/session'
import { apiClient } from '../graphql/client/core' import { apiClient } from '../graphql/client/core'
import { Shout } from '../graphql/schema/core.gen' import { Shout } from '../graphql/schema/core.gen'
import { router } from '../stores/router' import { router, useRouter } from '../stores/router'
import { redirectPage } from '@nanostores/router' import { redirectPage } from '@nanostores/router'
import { useSnackbar } from '../context/snackbar' import { useSnackbar } from '../context/snackbar'
@ -33,6 +33,7 @@ const getContentTypeTitle = (layout: LayoutType) => {
export const EditPage = () => { export const EditPage = () => {
const { t } = useLocalize() const { t } = useLocalize()
const { session } = useSession() const { session } = useSession()
const { page } = useRouter()
const snackbar = useSnackbar() const snackbar = useSnackbar()
const fail = async (error: string) => { const fail = async (error: string) => {
@ -45,12 +46,22 @@ export const EditPage = () => {
const [shoutId, setShoutId] = createSignal<number>(0) const [shoutId, setShoutId] = createSignal<number>(0)
const [shout, setShout] = createSignal<Shout>() const [shout, setShout] = createSignal<Shout>()
onMount(() => { createEffect(
const shoutId = window.location.pathname.split('/').pop() on(
const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10) () => page(),
console.debug(`editing shout ${shoutIdFromUrl}`) (p) => {
if (shoutIdFromUrl) setShoutId(shoutIdFromUrl) if (p?.path) {
}) console.debug(p?.path)
const shoutId = p?.path.split('/').pop()
const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10)
console.debug(`editing shout ${shoutIdFromUrl}`)
if (shoutIdFromUrl) {
setShoutId(shoutIdFromUrl)
}
}
},
),
)
createEffect( createEffect(
on([session, shout, shoutId], async ([ses, sh, shid]) => { on([session, shout, shoutId], async ([ses, sh, shid]) => {
@ -63,6 +74,7 @@ export const EditPage = () => {
} }
} }
}), }),
{ defer: true },
) )
const title = createMemo(() => { const title = createMemo(() => {