Merge branch 'fix/topic-header' of github.com:Discours/discoursio-webapp into fix/topic-header
This commit is contained in:
commit
6b0fa86c21
4
public/icons/expert.svg
Normal file
4
public/icons/expert.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M11.9967 4.51318C11.5931 4.51318 11.1868 4.59652 10.8118 4.75798L7.9056 6.01058L9.83268 6.81266L11.4056 6.13558C11.5931 6.05225 11.7962 6.01058 11.9993 6.01058C12.2025 6.01058 12.4056 6.05225 12.5931 6.13558L20.5801 9.57829C20.6504 9.60693 20.6947 9.67464 20.6947 9.75016C20.6947 9.82568 20.6504 9.89339 20.5801 9.92204L12.5931 13.3647C12.2181 13.5262 11.7806 13.5262 11.4056 13.3647L3.41862 9.92204C3.34831 9.89339 3.30404 9.82568 3.30404 9.75016C3.30404 9.67464 3.34831 9.60693 3.41862 9.57829L6.47591 8.26058L11.7103 10.4429C11.804 10.4819 11.903 10.5002 11.9993 10.5002C12.291 10.5002 12.5723 10.3283 12.6921 10.0392C12.8509 9.65641 12.6712 9.21631 12.2884 9.05746L8.39258 7.43506L8.39518 7.43246L6.4681 6.63037L2.42643 8.37516C1.87435 8.60954 1.51758 9.1512 1.51758 9.75016C1.51758 10.3491 1.87435 10.8908 2.42643 11.1252L4.87435 12.1825V18.5679C4.64779 18.7371 4.49935 19.008 4.49935 19.3127V20.8127C4.49935 21.3309 4.91862 21.7502 5.43685 21.7502H5.81185C6.33008 21.7502 6.74935 21.3309 6.74935 20.8127V19.3127C6.74935 19.008 6.60091 18.7371 6.37435 18.5679V17.1512C7.42904 17.909 9.2181 18.7502 11.9993 18.7502C15.5384 18.7502 17.4889 17.3856 18.3353 16.5705C18.8379 16.0887 19.1243 15.4064 19.1243 14.6955V12.1825L21.5723 11.1252C22.1243 10.8908 22.4811 10.3491 22.4811 9.75016C22.4811 9.1512 22.1243 8.60954 21.5723 8.37516L13.1868 4.75798C12.8092 4.59652 12.403 4.51318 11.9967 4.51318ZM6.37435 12.8283L10.8118 14.7424C11.1895 14.9064 11.5931 14.9845 11.9993 14.9845C12.4056 14.9845 12.8092 14.9064 13.1868 14.7424L17.6243 12.8283V14.6955C17.6243 15.0002 17.5046 15.2892 17.2962 15.4897C16.6113 16.146 15.015 17.2502 11.9993 17.2502C8.98372 17.2502 7.38737 16.146 6.70247 15.4897C6.49414 15.2892 6.37435 15.0002 6.37435 14.6955V12.8283Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
|
@ -93,6 +93,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",
|
||||
|
@ -432,6 +433,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",
|
||||
|
@ -484,7 +486,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",
|
||||
|
|
|
@ -97,6 +97,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": "Скопировать ссылку",
|
||||
|
@ -507,7 +508,6 @@
|
|||
"cancel": "отменить",
|
||||
"collections": "коллекции",
|
||||
"community": "сообщество",
|
||||
"contents": "оглавление",
|
||||
"create_chat": "Создать чат",
|
||||
"create_group": "Создать группу",
|
||||
"delimiter": "разделитель",
|
||||
|
@ -564,6 +564,7 @@
|
|||
"topicKeywords": "{topic}, Discours.io, статьи, журналистика, исследования",
|
||||
"topics": "темы",
|
||||
"user already exist": "пользователь уже существует",
|
||||
"User was not found": "Пользователь не найден",
|
||||
"verified": "уже подтверждён",
|
||||
"video": "видео",
|
||||
"view": "просмотр",
|
||||
|
|
|
@ -18,9 +18,8 @@
|
|||
|
||||
.authorName {
|
||||
@include font-size(4rem);
|
||||
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.2em;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.authorAbout {
|
||||
|
@ -432,3 +431,16 @@
|
|||
.listWrapper {
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.subscribersContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: 1.4rem;
|
||||
gap: 1rem;
|
||||
margin-top: 0;
|
||||
white-space: nowrap;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,12 +127,14 @@ export const AuthorCard = (props: Props) => {
|
|||
<div class={styles.authorAbout} innerHTML={props.author.bio} />
|
||||
</Show>
|
||||
<Show when={props.followers?.length > 0 || props.following?.length > 0}>
|
||||
<div class={styles.subscribersContainer}>
|
||||
<Subscribers
|
||||
followers={props.followers}
|
||||
followersAmount={props.author?.stat?.followers}
|
||||
following={props.following}
|
||||
followingAmount={props.author?.stat?.authors}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<ShowOnlyOnClient>
|
||||
|
|
|
@ -16,14 +16,15 @@
|
|||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
font-size: inherit;
|
||||
font-weight: 500;
|
||||
gap: 0.8rem;
|
||||
padding: 8px 16px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
|
||||
&.soon {
|
||||
|
@ -32,11 +33,29 @@
|
|||
gap: 0.6rem;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
|
||||
.icon {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--black-500);
|
||||
color: var(--black-50) !important;
|
||||
|
||||
.icon {
|
||||
filter: invert(1);
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex: 0 2.4rem;
|
||||
min-width: 2.4rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { clsx } from 'clsx'
|
|||
import { Show, createSignal } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Popup } from '../../_shared/Popup'
|
||||
import { SoonChip } from '../../_shared/SoonChip'
|
||||
|
||||
|
@ -38,7 +39,8 @@ export const FeedArticlePopup = (props: Props) => {
|
|||
setHidePopup(true)
|
||||
}}
|
||||
>
|
||||
{t('Share')}
|
||||
<Icon name="share-outline" class={styles.icon} />
|
||||
<div class={styles.title}>{t('Share')}</div>
|
||||
</button>
|
||||
</li>
|
||||
<Show when={!props.canEdit}>
|
||||
|
@ -51,7 +53,8 @@ export const FeedArticlePopup = (props: Props) => {
|
|||
setHidePopup(true)
|
||||
}}
|
||||
>
|
||||
{t('Help to edit')}
|
||||
<Icon name="pencil-outline" class={styles.icon} />
|
||||
<div class={styles.title}>{t('Help to edit')}</div>
|
||||
</button>
|
||||
</li>
|
||||
</Show>
|
||||
|
@ -64,19 +67,24 @@ export const FeedArticlePopup = (props: Props) => {
|
|||
setHidePopup(false)
|
||||
}}
|
||||
>
|
||||
{t('Invite experts')}
|
||||
<Icon name="expert" class={styles.icon} />
|
||||
<div class={styles.title}>{t('Invite experts')}</div>
|
||||
</button>
|
||||
</li>
|
||||
<Show when={!props.canEdit}>
|
||||
<li>
|
||||
<button class={clsx(styles.action, styles.soon)} role="button">
|
||||
{t('Subscribe to comments')} <SoonChip />
|
||||
<Icon name="bell-white" class={styles.icon} />
|
||||
<div class={styles.title}>{t('Subscribe to comments')}</div>
|
||||
<SoonChip />
|
||||
</button>
|
||||
</li>
|
||||
</Show>
|
||||
<li>
|
||||
<button class={clsx(styles.action, styles.soon)} role="button">
|
||||
{t('Add to bookmarks')} <SoonChip />
|
||||
<Icon name="bookmark" class={styles.icon} />
|
||||
<div class={styles.title}>{t('Add to bookmarks')}</div>
|
||||
<SoonChip />
|
||||
</button>
|
||||
</li>
|
||||
{/*<Show when={!props.canEdit}>*/}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()}>
|
||||
|
|
|
@ -44,16 +44,20 @@
|
|||
}
|
||||
|
||||
.topicDetails {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: 1.4rem;
|
||||
justify-content: center;
|
||||
gap: 4rem;
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.topicDetailsItem {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-right: 1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.topicDetailsIcon {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,11 +1,4 @@
|
|||
import {
|
||||
Author,
|
||||
AuthorsBy,
|
||||
LoadShoutsOptions,
|
||||
QueryLoad_Authors_ByArgs,
|
||||
Shout,
|
||||
Topic,
|
||||
} from '../../graphql/schema/core.gen'
|
||||
import { Author, AuthorsBy, LoadShoutsOptions, Shout, Topic } from '../../graphql/schema/core.gen'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
||||
|
|
|
@ -1,19 +1,8 @@
|
|||
.subscribersContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: 1.4rem;
|
||||
margin-top: 1.5rem;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.subscribers {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
margin: 0 2% 1rem;
|
||||
margin: 0 1rem 0 0;
|
||||
vertical-align: top;
|
||||
border-bottom: unset !important;
|
||||
|
||||
|
@ -44,7 +33,6 @@
|
|||
|
||||
.subscribersCounter {
|
||||
font-weight: 500;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -55,3 +43,8 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subscribersList {
|
||||
display: flex;
|
||||
margin-right: 0.6rem;
|
||||
}
|
||||
|
|
|
@ -18,12 +18,14 @@ export const Subscribers = (props: Props) => {
|
|||
const { t } = useLocalize()
|
||||
|
||||
return (
|
||||
<div class={styles.subscribersContainer}>
|
||||
<>
|
||||
<a href="?m=followers" class={styles.subscribers}>
|
||||
<Show when={props.followers && props.followers.length > 0}>
|
||||
<div class={styles.subscribersList}>
|
||||
<For each={props.followers.slice(0, 3)}>
|
||||
{(f) => <Userpic size={'XS'} name={f.name} userpic={f.pic} class={styles.subscribersItem} />}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
<div class={styles.subscribersCounter}>
|
||||
{t('SubscriberWithCount', {
|
||||
|
@ -34,19 +36,25 @@ export const Subscribers = (props: Props) => {
|
|||
|
||||
<a href="?m=following" class={styles.subscribers}>
|
||||
<Show when={props.following && props.following.length > 0}>
|
||||
<div class={styles.subscribersList}>
|
||||
<For each={props.following.slice(0, 3)}>
|
||||
{(f) => {
|
||||
if ('name' in f) {
|
||||
return <Userpic size={'XS'} name={f.name} userpic={f.pic} class={styles.subscribersItem} />
|
||||
return (
|
||||
<Userpic size={'XS'} name={f.name} userpic={f.pic} class={styles.subscribersItem} />
|
||||
)
|
||||
}
|
||||
|
||||
if ('title' in f) {
|
||||
return <Userpic size={'XS'} name={f.title} userpic={f.pic} class={styles.subscribersItem} />
|
||||
return (
|
||||
<Userpic size={'XS'} name={f.title} userpic={f.pic} class={styles.subscribersItem} />
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
<div class={styles.subscribersCounter}>
|
||||
{t('SubscriptionWithCount', {
|
||||
|
@ -54,6 +62,6 @@ export const Subscribers = (props: Props) => {
|
|||
})}
|
||||
</div>
|
||||
</a>
|
||||
</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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -70,6 +70,7 @@ export const EditPage = () => {
|
|||
}
|
||||
}
|
||||
}),
|
||||
{ defer: true },
|
||||
)
|
||||
|
||||
const title = createMemo(() => {
|
||||
|
|
|
@ -54,7 +54,7 @@ export const TopicPage = (props: PageProps) => {
|
|||
const usePrerenderedData = props.topic?.slug === slug()
|
||||
|
||||
return (
|
||||
<PageLayout title={props.seo.title}>
|
||||
<PageLayout title={props.seo?.title}>
|
||||
<ReactionsProvider>
|
||||
<Show when={isLoaded()} fallback={<Loading />}>
|
||||
<TopicView
|
||||
|
|
Loading…
Reference in New Issue
Block a user