Merge branch 'dev' into hotfix/following
This commit is contained in:
commit
58ec520c3e
|
@ -91,6 +91,7 @@
|
|||
"Community Principles": "Community Principles",
|
||||
"Community values and rules of engagement for the open editorial team": "Community values and rules of engagement for the open editorial team",
|
||||
"Confirm": "Confirm",
|
||||
"Contents": "Contents",
|
||||
"Contribute to free samizdat. Support Discours - an independent non-profit publication that works only for you. Become a pillar of the open newsroom": "Contribute to free samizdat. Support Discours - an independent non-profit publication that works only for you. Become a pillar of the open newsroom",
|
||||
"Cooperate": "Cooperate",
|
||||
"Copy link": "Copy link",
|
||||
|
@ -417,6 +418,7 @@
|
|||
"Username": "Username",
|
||||
"Userpic": "Userpic",
|
||||
"Users": "Users",
|
||||
"User was not found": "User was not found",
|
||||
"Video format not supported": "Video format not supported",
|
||||
"Video": "Video",
|
||||
"Views": "Views",
|
||||
|
@ -469,7 +471,6 @@
|
|||
"cancel": "cancel",
|
||||
"collections": "collections",
|
||||
"community": "community",
|
||||
"contents": "contents",
|
||||
"delimiter": "delimiter",
|
||||
"discussion": "Discours",
|
||||
"dogma keywords": "Discours.io, dogma, editorial principles, code of ethics, journalism, community",
|
||||
|
|
|
@ -95,6 +95,7 @@
|
|||
"Community Principles": "Принципы сообщества",
|
||||
"Community values and rules of engagement for the open editorial team": "Ценности сообщества и правила взаимодействия открытой редакции",
|
||||
"Confirm": "Подтвердить",
|
||||
"Contents": "Оглавление",
|
||||
"Contribute to free samizdat. Support Discours - an independent non-profit publication that works only for you. Become a pillar of the open newsroom": "Внесите вклад в свободный самиздат. Поддержите Дискурс — независимое некоммерческое издание, которое работает только для вас. Станьте опорой открытой редакции",
|
||||
"Cooperate": "Соучаствовать",
|
||||
"Copy link": "Скопировать ссылку",
|
||||
|
@ -492,7 +493,6 @@
|
|||
"cancel": "отменить",
|
||||
"collections": "коллекции",
|
||||
"community": "сообщество",
|
||||
"contents": "оглавление",
|
||||
"create_chat": "Создать чат",
|
||||
"create_group": "Создать группу",
|
||||
"delimiter": "разделитель",
|
||||
|
@ -549,6 +549,7 @@
|
|||
"topicKeywords": "{topic}, Discours.io, статьи, журналистика, исследования",
|
||||
"topics": "темы",
|
||||
"user already exist": "пользователь уже существует",
|
||||
"User was not found": "Пользователь не найден",
|
||||
"verified": "уже подтверждён",
|
||||
"video": "видео",
|
||||
"view": "просмотр",
|
||||
|
|
|
@ -95,33 +95,37 @@ export const LoginForm = () => {
|
|||
|
||||
try {
|
||||
const { errors } = await signIn({ email: email(), password: password() })
|
||||
console.error('[signIn errors]', errors)
|
||||
if (errors?.length > 0) {
|
||||
if (
|
||||
errors.some(
|
||||
(error) =>
|
||||
error.message.includes('bad user credentials') || error.message.includes('user not found'),
|
||||
)
|
||||
) {
|
||||
console.warn('[signIn] errors: ', errors)
|
||||
errors.forEach((error) => {
|
||||
switch (error.message) {
|
||||
case 'user has not signed up email & password': {
|
||||
setValidationErrors((prev) => ({
|
||||
...prev,
|
||||
password: t('Something went wrong, check email and password'),
|
||||
}))
|
||||
} else if (errors.some((error) => error.message.includes('user not found'))) {
|
||||
setSubmitError('Пользователь не найден')
|
||||
} else if (errors.some((error) => error.message.includes('email not verified'))) {
|
||||
break
|
||||
}
|
||||
case 'user not found': {
|
||||
setValidationErrors((prev) => ({ ...prev, email: t('User was not found') }))
|
||||
break
|
||||
}
|
||||
case 'email not verified': {
|
||||
setValidationErrors((prev) => ({ ...prev, email: t('This email is not verified') }))
|
||||
break
|
||||
}
|
||||
default:
|
||||
setSubmitError(
|
||||
<div class={styles.info}>
|
||||
{t('This email is not verified')}
|
||||
{t('Error', errors[0].message)}
|
||||
{'. '}
|
||||
<span class={'link'} onClick={handleSendLinkAgainClick}>
|
||||
{t('Send link again')}
|
||||
</span>
|
||||
</div>,
|
||||
)
|
||||
} else {
|
||||
setSubmitError(t('Error', errors[0].message))
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
hideModal()
|
||||
|
|
|
@ -103,7 +103,13 @@ export const HeaderAuth = (props: Props) => {
|
|||
<div class={clsx('col-auto col-lg-7', styles.usernav)}>
|
||||
<div class={styles.userControl}>
|
||||
<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')}>
|
||||
<span class={styles.textLabel}>{t('Create post')}</span>
|
||||
<Icon name="pencil-outline" class={styles.icon} />
|
||||
|
@ -210,11 +216,17 @@ export const HeaderAuth = (props: Props) => {
|
|||
</Show>
|
||||
|
||||
<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')}>
|
||||
<span class={styles.textLabel}>{t('Create post')}</span>
|
||||
<Icon name="pencil" class={styles.icon} />
|
||||
<Icon name="pencil" class={clsx(styles.icon, styles.iconHover)} />
|
||||
<Icon name="pencil-outline" class={styles.icon} />
|
||||
<Icon name="pencil-outline-hover" class={clsx(styles.icon, styles.iconHover)} />
|
||||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
|
@ -227,7 +239,7 @@ export const HeaderAuth = (props: Props) => {
|
|||
<a href="?m=auth&mode=login">
|
||||
<span class={styles.textLabel}>{t('Enter')}</span>
|
||||
<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>
|
||||
</div>
|
||||
</Show>
|
||||
|
|
|
@ -157,7 +157,7 @@
|
|||
color: #000;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-weight: 500;
|
||||
line-height: 1.8rem;
|
||||
text-align: left;
|
||||
vertical-align: bottom;
|
||||
|
|
|
@ -86,7 +86,7 @@ export const TableOfContents = (props: Props) => {
|
|||
<Show when={isVisible()}>
|
||||
<div class={styles.TableOfContentsContainerInner}>
|
||||
<div class={styles.TableOfContentsHeader}>
|
||||
<p class={styles.TableOfContentsHeading}>{t('contents')}</p>
|
||||
<p class={styles.TableOfContentsHeading}>{t('Contents')}</p>
|
||||
</div>
|
||||
<ul class={styles.TableOfContentsHeadingsList}>
|
||||
<For each={headings()}>
|
||||
|
|
|
@ -45,7 +45,6 @@
|
|||
|
||||
.info {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
border: none;
|
||||
|
||||
// display: flex;
|
||||
|
@ -63,13 +62,11 @@
|
|||
|
||||
.title {
|
||||
@include font-size(2.2rem);
|
||||
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.description {
|
||||
@include font-size(1.6rem);
|
||||
|
||||
line-height: 1.4;
|
||||
margin: 0.8rem 0;
|
||||
-webkit-line-clamp: 2;
|
||||
|
@ -107,7 +104,6 @@
|
|||
|
||||
.title {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
font-weight: 500;
|
||||
line-height: 1em;
|
||||
color: var(--blue-500);
|
||||
|
@ -116,9 +112,7 @@
|
|||
|
||||
.description {
|
||||
color: var(--black-400);
|
||||
|
||||
@include font-size(1.2rem);
|
||||
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ export const TopicBadge = (props: Props) => {
|
|||
lang() === 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.TopicBadge, props.subscriptionsMode)}>
|
||||
<div class={clsx(styles.TopicBadge, { [styles.TopicBadgeSubscriptionsMode]: props.subscriptionsMode })}>
|
||||
<div class={styles.content}>
|
||||
<div class={styles.basicInfo}>
|
||||
<Show when={props.subscriptionsMode}>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { clsx } from 'clsx'
|
|||
import deepEqual from 'fast-deep-equal'
|
||||
import { Accessor, Show, createMemo, createSignal, lazy, onCleanup, onMount } from 'solid-js'
|
||||
import { createStore } from 'solid-js/store'
|
||||
import { throttle } from 'throttle-debounce'
|
||||
import { debounce } from 'throttle-debounce'
|
||||
|
||||
import { ShoutForm, useEditorContext } from '../../../context/editor'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
|
@ -42,9 +42,8 @@ export const EMPTY_TOPIC: Topic = {
|
|||
slug: '',
|
||||
}
|
||||
|
||||
const THROTTLING_INTERVAL = 2000
|
||||
const AUTO_SAVE_INTERVAL = 5000
|
||||
const AUTO_SAVE_DELAY = 5000
|
||||
const AUTO_SAVE_DELAY = 3000
|
||||
|
||||
const handleScrollTopButtonClick = (e) => {
|
||||
e.preventDefault()
|
||||
window.scrollTo({
|
||||
|
@ -104,6 +103,8 @@ export const EditView = (props: Props) => {
|
|||
return JSON.parse(form.media || '[]')
|
||||
})
|
||||
|
||||
const [hasChanges, setHasChanges] = createSignal(false)
|
||||
|
||||
onMount(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 0)
|
||||
|
@ -113,7 +114,7 @@ export const EditView = (props: Props) => {
|
|||
onCleanup(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
|
||||
const handleBeforeUnload = (event) => {
|
||||
if (!deepEqual(prevForm, form)) {
|
||||
event.returnValue = t(
|
||||
|
@ -127,8 +128,8 @@ export const EditView = (props: Props) => {
|
|||
})
|
||||
|
||||
const handleTitleInputChange = (value: string) => {
|
||||
setForm('title', value)
|
||||
setForm('slug', slugify(value))
|
||||
handleInputChange('title', value)
|
||||
handleInputChange('slug', slugify(value))
|
||||
if (value) {
|
||||
setFormErrors('title', '')
|
||||
}
|
||||
|
@ -136,21 +137,21 @@ export const EditView = (props: Props) => {
|
|||
|
||||
const handleAddMedia = (data) => {
|
||||
const newMedia = [...mediaItems(), ...data]
|
||||
setForm('media', JSON.stringify(newMedia))
|
||||
handleInputChange('media', JSON.stringify(newMedia))
|
||||
}
|
||||
const handleSortedMedia = (data) => {
|
||||
setForm('media', JSON.stringify(data))
|
||||
handleInputChange('media', JSON.stringify(data))
|
||||
}
|
||||
|
||||
const handleMediaDelete = (index) => {
|
||||
const copy = [...mediaItems()]
|
||||
copy.splice(index, 1)
|
||||
setForm('media', JSON.stringify(copy))
|
||||
handleInputChange('media', JSON.stringify(copy))
|
||||
}
|
||||
|
||||
const handleMediaChange = (index, value) => {
|
||||
const updated = mediaItems().map((item, idx) => (idx === index ? value : item))
|
||||
setForm('media', JSON.stringify(updated))
|
||||
handleInputChange('media', JSON.stringify(updated))
|
||||
}
|
||||
|
||||
const [baseAudioFields, setBaseAudioFields] = createSignal({
|
||||
|
@ -162,7 +163,7 @@ export const EditView = (props: Props) => {
|
|||
const handleBaseFieldsChange = (key, value) => {
|
||||
if (mediaItems().length > 0) {
|
||||
const updated = mediaItems().map((media) => ({ ...media, [key]: value }))
|
||||
setForm('media', JSON.stringify(updated))
|
||||
handleInputChange('media', JSON.stringify(updated))
|
||||
} else {
|
||||
setBaseAudioFields({ ...baseAudioFields(), [key]: value })
|
||||
}
|
||||
|
@ -182,34 +183,32 @@ export const EditView = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
let autoSaveTimeOutId: number | string | NodeJS.Timeout
|
||||
|
||||
const autoSave = async () => {
|
||||
const hasChanges = !deepEqual(form, prevForm)
|
||||
const hasTopic = Boolean(form.mainTopic)
|
||||
if (hasChanges || hasTopic) {
|
||||
console.log('autoSave called')
|
||||
if (hasChanges()) {
|
||||
console.debug('saving draft', form)
|
||||
setSaving(true)
|
||||
saveDraftToLocalStorage(form)
|
||||
await saveDraft(form)
|
||||
setPrevForm(clone(form))
|
||||
setTimeout(() => setSaving(false), AUTO_SAVE_DELAY)
|
||||
setSaving(false)
|
||||
setHasChanges(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Throttle the autoSave function
|
||||
const throttledAutoSave = throttle(THROTTLING_INTERVAL, autoSave)
|
||||
const debouncedAutoSave = debounce(AUTO_SAVE_DELAY, autoSave)
|
||||
|
||||
const autoSaveRecursive = () => {
|
||||
autoSaveTimeOutId = setTimeout(() => {
|
||||
throttledAutoSave()
|
||||
autoSaveRecursive()
|
||||
}, AUTO_SAVE_INTERVAL)
|
||||
const handleInputChange = (key, value) => {
|
||||
console.log(`[handleInputChange] ${key}: ${value}`)
|
||||
setForm(key, value)
|
||||
setHasChanges(true)
|
||||
debouncedAutoSave()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
autoSaveRecursive()
|
||||
onCleanup(() => clearTimeout(autoSaveTimeOutId))
|
||||
onCleanup(() => {
|
||||
debouncedAutoSave.cancel()
|
||||
})
|
||||
})
|
||||
|
||||
const showSubtitleInput = () => {
|
||||
|
@ -310,7 +309,7 @@ export const EditView = (props: Props) => {
|
|||
subtitleInput.current = el
|
||||
}}
|
||||
allowEnterKey={false}
|
||||
value={(value) => setForm('subtitle', value || '')}
|
||||
value={(value) => handleInputChange('subtitle', value || '')}
|
||||
class={styles.subtitleInput}
|
||||
placeholder={t('Subheader')}
|
||||
initialValue={form.subtitle || ''}
|
||||
|
@ -324,7 +323,7 @@ export const EditView = (props: Props) => {
|
|||
smallHeight={true}
|
||||
placeholder={t('A short introduction to keep the reader interested')}
|
||||
initialContent={form.lead}
|
||||
onChange={(value) => setForm('lead', value)}
|
||||
onChange={(value) => handleInputChange('lead', value)}
|
||||
/>
|
||||
</Show>
|
||||
</Show>
|
||||
|
@ -345,7 +344,7 @@ export const EditView = (props: Props) => {
|
|||
}
|
||||
isMultiply={false}
|
||||
fileType={'image'}
|
||||
onUpload={(val) => setForm('coverImageUrl', val[0].url)}
|
||||
onUpload={(val) => handleInputChange('coverImageUrl', val[0].url)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
@ -362,7 +361,7 @@ export const EditView = (props: Props) => {
|
|||
<div
|
||||
ref={triggerRef}
|
||||
class={styles.delete}
|
||||
onClick={() => setForm('coverImageUrl', null)}
|
||||
onClick={() => handleInputChange('coverImageUrl', null)}
|
||||
>
|
||||
<Icon name="close-white" />
|
||||
</div>
|
||||
|
@ -408,7 +407,7 @@ export const EditView = (props: Props) => {
|
|||
<Editor
|
||||
shoutId={form.shoutId}
|
||||
initialContent={form.body}
|
||||
onChange={(body) => setForm('body', body)}
|
||||
onChange={(body) => handleInputChange('body', body)}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
@ -30,19 +30,19 @@ const ConnectContext = createContext<ConnectContextType>()
|
|||
|
||||
export const ConnectProvider = (props: { children: JSX.Element }) => {
|
||||
const [messageHandlers, setHandlers] = createSignal<MessageHandler[]>([])
|
||||
// const [messages, setMessages] = createSignal<Array<SSEMessage>>([]);
|
||||
const [connected, setConnected] = createSignal(false)
|
||||
const { session } = useSession()
|
||||
const [retried, setRetried] = createSignal<number>(0)
|
||||
|
||||
const addHandler = (handler: MessageHandler) => {
|
||||
setHandlers((hhh) => [...hhh, handler])
|
||||
}
|
||||
|
||||
const [retried, setRetried] = createSignal<number>(0)
|
||||
createEffect(async () => {
|
||||
const token = session()?.access_token
|
||||
if (token && !connected()) {
|
||||
if (token && !connected() && retried() <= RECONNECT_TIMES) {
|
||||
console.info('[context.connect] init SSE connection')
|
||||
try {
|
||||
await fetchEventSource('https://connect.discours.io', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
@ -52,31 +52,35 @@ export const ConnectProvider = (props: { children: JSX.Element }) => {
|
|||
onmessage(event) {
|
||||
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))
|
||||
},
|
||||
async onopen(response) {
|
||||
onopen: (response) => {
|
||||
console.log('[context.connect] SSE connection opened', response)
|
||||
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
|
||||
setConnected(true)
|
||||
} else if (response.status === 401) {
|
||||
throw new Error('SSE: cannot connect to real-time updates')
|
||||
} else {
|
||||
setRetried((r) => r + 1)
|
||||
throw new Error(`SSE: failed to connect ${retried()} times`)
|
||||
setRetried(0)
|
||||
return Promise.resolve()
|
||||
}
|
||||
return Promise.reject(`SSE: cannot connect to real-time updates, status: ${response.status}`)
|
||||
},
|
||||
onclose() {
|
||||
console.log('[context.connect] SSE connection closed by server')
|
||||
setConnected(false)
|
||||
},
|
||||
onerror(err) {
|
||||
if (err.message === 'unauthorized' || retried() > RECONNECT_TIMES) {
|
||||
throw err // rethrow to stop the operation
|
||||
if (retried() < RECONNECT_TIMES) {
|
||||
setRetried((r) => r + 1)
|
||||
}
|
||||
},
|
||||
onerror(err) {
|
||||
console.error('[context.connect] SSE connection error:', err)
|
||||
setConnected(false)
|
||||
if (retried() < RECONNECT_TIMES) {
|
||||
setRetried((r) => r + 1)
|
||||
} else throw Error(err)
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[context.connect] SSE connection failed:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -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 { Loading } from '../components/_shared/Loading'
|
||||
|
@ -7,7 +7,7 @@ import { useLocalize } from '../context/localize'
|
|||
import { useSession } from '../context/session'
|
||||
import { apiClient } from '../graphql/client/core'
|
||||
import { Shout } from '../graphql/schema/core.gen'
|
||||
import { router } from '../stores/router'
|
||||
import { router, useRouter } from '../stores/router'
|
||||
|
||||
import { redirectPage } from '@nanostores/router'
|
||||
import { useSnackbar } from '../context/snackbar'
|
||||
|
@ -33,6 +33,7 @@ const getContentTypeTitle = (layout: LayoutType) => {
|
|||
export const EditPage = () => {
|
||||
const { t } = useLocalize()
|
||||
const { session } = useSession()
|
||||
const { page } = useRouter()
|
||||
const snackbar = useSnackbar()
|
||||
|
||||
const fail = async (error: string) => {
|
||||
|
@ -45,12 +46,22 @@ export const EditPage = () => {
|
|||
const [shoutId, setShoutId] = createSignal<number>(0)
|
||||
const [shout, setShout] = createSignal<Shout>()
|
||||
|
||||
onMount(() => {
|
||||
const shoutId = window.location.pathname.split('/').pop()
|
||||
createEffect(
|
||||
on(
|
||||
() => page(),
|
||||
(p) => {
|
||||
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)
|
||||
})
|
||||
if (shoutIdFromUrl) {
|
||||
setShoutId(shoutIdFromUrl)
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on([session, shout, shoutId], async ([ses, sh, shid]) => {
|
||||
|
@ -63,6 +74,7 @@ export const EditPage = () => {
|
|||
}
|
||||
}
|
||||
}),
|
||||
{ defer: true },
|
||||
)
|
||||
|
||||
const title = createMemo(() => {
|
||||
|
|
Loading…
Reference in New Issue
Block a user