Merge pull request #447 from Discours/hotfix/editor-permission

use-session in editor
This commit is contained in:
Tony 2024-05-06 17:33:04 +03:00 committed by GitHub
commit 8f09d6fc54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 207 additions and 201 deletions

View File

@ -40,16 +40,12 @@ import { InboxPage } from '../pages/inbox.page'
import { HomePage } from '../pages/index.page' import { HomePage } from '../pages/index.page'
import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page' import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
import { ProfileSettingsPage } from '../pages/profile/profileSettings.page' import { ProfileSettingsPage } from '../pages/profile/profileSettings.page'
//TODO: ProfileSubscriptionsPage - garbage code?
import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page' import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page'
import { SearchPage } from '../pages/search.page' import { SearchPage } from '../pages/search.page'
import { TopicPage } from '../pages/topic.page' import { TopicPage } from '../pages/topic.page'
import { ROUTES, useRouter } from '../stores/router' import { ROUTES, useRouter } from '../stores/router'
import { MODALS, showModal } from '../stores/ui' import { MODALS, showModal } from '../stores/ui'
// TODO: lazy load
// const SomePage = lazy(() => import('./Pages/SomePage'))
const pagesMap: Record<keyof typeof ROUTES, Component<PageProps>> = { const pagesMap: Record<keyof typeof ROUTES, Component<PageProps>> = {
author: AuthorPage, author: AuthorPage,
authorComments: AuthorPage, authorComments: AuthorPage,

View File

@ -75,7 +75,7 @@ export const FullArticle = (props: Props) => {
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false) const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false) const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
const { t, formatDate, lang } = useLocalize() const { t, formatDate, lang } = useLocalize()
const { author, session, isAuthenticated, requireAuthentication } = useSession() const { author, session, requireAuthentication } = useSession()
const formattedDate = createMemo(() => formatDate(new Date(props.article.published_at * 1000))) const formattedDate = createMemo(() => formatDate(new Date(props.article.published_at * 1000)))
@ -561,7 +561,7 @@ export const FullArticle = (props: Props) => {
/> />
</div> </div>
<Show when={isAuthenticated() && !canEdit()}> <Show when={author()?.id && !canEdit()}>
<div class={styles.help}> <div class={styles.help}>
<button class="button">{t('Cooperate')}</button> <button class="button">{t('Cooperate')}</button>
</div> </div>

View File

@ -12,7 +12,7 @@ type Props = {
} }
export const AuthGuard = (props: Props) => { export const AuthGuard = (props: Props) => {
const { isAuthenticated, isSessionLoaded } = useSession() const { author, isSessionLoaded } = useSession()
const { changeSearchParams } = useRouter<RootSearchParams & AuthModalSearchParams>() const { changeSearchParams } = useRouter<RootSearchParams & AuthModalSearchParams>()
createEffect(() => { createEffect(() => {
@ -20,7 +20,7 @@ export const AuthGuard = (props: Props) => {
return return
} }
if (isSessionLoaded()) { if (isSessionLoaded()) {
if (isAuthenticated()) { if (author()?.id) {
hideModal() hideModal()
} else { } else {
changeSearchParams( changeSearchParams(
@ -37,5 +37,5 @@ export const AuthGuard = (props: Props) => {
} }
}) })
return <Show when={(isSessionLoaded() && isAuthenticated()) || props.disabled}>{props.children}</Show> return <Show when={(isSessionLoaded() && author()?.id) || props.disabled}>{props.children}</Show>
} }

View File

@ -119,6 +119,9 @@ export const AuthorBadge = (props: Props) => {
<Show when={props.author?.stat.shouts > 0}> <Show when={props.author?.stat.shouts > 0}>
<div>{t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}</div> <div>{t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}</div>
</Show> </Show>
<Show when={props.author?.stat.comments > 0}>
<div>{t('CommentsWithCount', { count: props.author.stat?.comments ?? 0 })}</div>
</Show>
<Show when={props.author?.stat.followers > 0}> <Show when={props.author?.stat.followers > 0}>
<div>{t('FollowersWithCount', { count: props.author.stat?.followers ?? 0 })}</div> <div>{t('FollowersWithCount', { count: props.author.stat?.followers ?? 0 })}</div>
</Show> </Show>

View File

@ -1,7 +1,7 @@
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { showModal } from '../../stores/ui' import { showModal } from '../../stores/ui'
import { AuthModalSearchParams } from '../Nav/AuthModal/types' import type { AuthModalSearchParams } from '../Nav/AuthModal/types'
import styles from './Hero.module.scss' import styles from './Hero.module.scss'

View File

@ -7,6 +7,7 @@ import { composeMediaItems } from '../../../utils/composeMediaItems'
import { AudioPlayer } from '../../Article/AudioPlayer' import { AudioPlayer } from '../../Article/AudioPlayer'
import { DropArea } from '../../_shared/DropArea' import { DropArea } from '../../_shared/DropArea'
// import { Buffer } from 'node:buffer'
import styles from './AudioUploader.module.scss' import styles from './AudioUploader.module.scss'
window.Buffer = Buffer window.Buffer = Buffer

View File

@ -30,9 +30,11 @@ const embedData = (data) => {
// biome-ignore lint/style/useForOf: <explanation> // biome-ignore lint/style/useForOf: <explanation>
for (let i = 0; i < attributes.length; i++) { for (let i = 0; i < attributes.length; i++) {
const attribute = attributes[i] const attribute = attributes.item(i)
if (attribute) {
result[attribute.name] = attribute.value result[attribute.name] = attribute.value
} }
}
return result return result
} }

View File

@ -23,8 +23,16 @@ type Props = {
export const Panel = (props: Props) => { export const Panel = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { isEditorPanelVisible, wordCounter, editorRef, form, toggleEditorPanel, saveShout, publishShout } = const {
useEditorContext() isEditorPanelVisible,
wordCounter,
editorRef,
form,
toggleEditorPanel,
saveShout,
saveDraft,
publishShout,
} = useEditorContext()
const containerRef: { current: HTMLElement } = { current: null } const containerRef: { current: HTMLElement } = { current: null }
const [isShortcutsVisible, setIsShortcutsVisible] = createSignal(false) const [isShortcutsVisible, setIsShortcutsVisible] = createSignal(false)
@ -43,7 +51,12 @@ export const Panel = (props: Props) => {
}) })
const handleSaveClick = () => { const handleSaveClick = () => {
const hasTopics = form.selectedTopics?.length > 0
if (hasTopics) {
saveShout(form) saveShout(form)
} else {
saveDraft(form)
}
} }
const html = useEditorHTML(() => editorRef.current()) const html = useEditorHTML(() => editorRef.current())

View File

@ -31,7 +31,7 @@ export const LoginForm = () => {
const [isSubmitting, setIsSubmitting] = createSignal(false) const [isSubmitting, setIsSubmitting] = createSignal(false)
const [password, setPassword] = createSignal('') const [password, setPassword] = createSignal('')
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({}) const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
// FIXME: use signal or remove
const [_isLinkSent, setIsLinkSent] = createSignal(false) const [_isLinkSent, setIsLinkSent] = createSignal(false)
const authFormRef: { current: HTMLFormElement } = { current: null } const authFormRef: { current: HTMLFormElement } = { current: null }
const { showSnackbar } = useSnackbar() const { showSnackbar } = useSnackbar()

View File

@ -32,6 +32,7 @@ export const RegisterForm = () => {
const { changeSearchParams } = useRouter<AuthModalSearchParams>() const { changeSearchParams } = useRouter<AuthModalSearchParams>()
const { t } = useLocalize() const { t } = useLocalize()
const { signUp, isRegistered, resendVerifyEmail } = useSession() const { signUp, isRegistered, resendVerifyEmail } = useSession()
// FIXME: use submit error data or remove signal
const [_submitError, setSubmitError] = createSignal('') const [_submitError, setSubmitError] = createSignal('')
const [fullName, setFullName] = createSignal('') const [fullName, setFullName] = createSignal('')
const [password, setPassword] = createSignal('') const [password, setPassword] = createSignal('')

View File

@ -59,7 +59,7 @@ export const Header = (props: Props) => {
const [isTopicsVisible, setIsTopicsVisible] = createSignal(false) const [isTopicsVisible, setIsTopicsVisible] = createSignal(false)
const [isZineVisible, setIsZineVisible] = createSignal(false) const [isZineVisible, setIsZineVisible] = createSignal(false)
const [isFeedVisible, setIsFeedVisible] = createSignal(false) const [isFeedVisible, setIsFeedVisible] = createSignal(false)
const { isAuthenticated } = useSession() const { session } = useSession()
const toggleFixed = () => setFixed(!fixed()) const toggleFixed = () => setFixed(!fixed())
@ -335,7 +335,7 @@ export const Header = (props: Props) => {
<Show when={props.title}> <Show when={props.title}>
<div <div
class={clsx(styles.articleControls, 'col-auto', { class={clsx(styles.articleControls, 'col-auto', {
[styles.articleControlsAuthorized]: isAuthenticated(), [styles.articleControlsAuthorized]: session()?.user?.id,
})} })}
> >
<SharePopup <SharePopup

View File

@ -32,14 +32,14 @@ const MD_WIDTH_BREAKPOINT = 992
export const HeaderAuth = (props: Props) => { export const HeaderAuth = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { page } = useRouter() const { page } = useRouter()
const { session, author, isAuthenticated, isSessionLoaded } = useSession() const { session, author, isSessionLoaded } = useSession()
const { unreadNotificationsCount, showNotificationsPanel } = useNotifications() const { unreadNotificationsCount, showNotificationsPanel } = useNotifications()
const { form, toggleEditorPanel, saveShout, publishShout } = useEditorContext() const { form, toggleEditorPanel, saveShout, saveDraft, publishShout } = useEditorContext()
const handleBellIconClick = (event: Event) => { const handleBellIconClick = (event: Event) => {
event.preventDefault() event.preventDefault()
if (!isAuthenticated()) { if (!author()?.id) {
showModal('auth') showModal('auth')
return return
} }
@ -48,19 +48,22 @@ export const HeaderAuth = (props: Props) => {
} }
const isEditorPage = createMemo(() => page().route === 'edit' || page().route === 'editSettings') const isEditorPage = createMemo(() => page().route === 'edit' || page().route === 'editSettings')
const isNotificationsVisible = createMemo(() => isAuthenticated() && !isEditorPage()) const isNotificationsVisible = createMemo(() => author()?.id && !isEditorPage())
const isSaveButtonVisible = createMemo(() => isAuthenticated() && isEditorPage()) const isSaveButtonVisible = createMemo(() => author()?.id && isEditorPage())
const isCreatePostButtonVisible = createMemo(() => !isEditorPage()) const isCreatePostButtonVisible = createMemo(() => !isEditorPage())
const isAuthenticatedControlsVisible = createMemo( const isAuthenticatedControlsVisible = createMemo(() => author()?.id && session()?.user?.email_verified)
() => isAuthenticated() && session()?.user?.email_verified,
)
const handleBurgerButtonClick = () => { const handleBurgerButtonClick = () => {
toggleEditorPanel() toggleEditorPanel()
} }
const _handleSaveButtonClick = () => { const handleSaveClick = () => {
const hasTopics = form.selectedTopics?.length > 0
if (hasTopics) {
saveShout(form) saveShout(form)
} else {
saveDraft(form)
}
} }
const [width, setWidth] = createSignal(0) const [width, setWidth] = createSignal(0)
@ -106,14 +109,8 @@ export const HeaderAuth = (props: Props) => {
<Show when={isSessionLoaded()} keyed={true}> <Show when={isSessionLoaded()} keyed={true}>
<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() && isAuthenticated()}> <Show when={isCreatePostButtonVisible() && author()?.id}>
<div <div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
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} />
@ -220,14 +217,8 @@ export const HeaderAuth = (props: Props) => {
</div> </div>
</Show> </Show>
<Show when={isCreatePostButtonVisible() && !isAuthenticated()}> <Show when={isCreatePostButtonVisible() && !author()?.id}>
<div <div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
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} />
@ -239,7 +230,7 @@ export const HeaderAuth = (props: Props) => {
<Show <Show
when={isAuthenticatedControlsVisible()} when={isAuthenticatedControlsVisible()}
fallback={ fallback={
<Show when={!isAuthenticated()}> <Show when={!author()?.id}>
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}> <div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
<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>
@ -250,13 +241,10 @@ export const HeaderAuth = (props: Props) => {
</Show> </Show>
} }
> >
<Show when={!isSaveButtonVisible()}> <Show
<div when={isSaveButtonVisible()}
class={clsx( fallback={
styles.userControlItem, <div class={clsx(styles.userControlItem)}>
// styles.userControlItemInbox
)}
>
<a href={getPagePath(router, 'inbox')}> <a href={getPagePath(router, 'inbox')}>
<div classList={{ entered: page().path === '/inbox' }}> <div classList={{ entered: page().path === '/inbox' }}>
<Icon name="inbox-white" class={styles.icon} /> <Icon name="inbox-white" class={styles.icon} />
@ -264,11 +252,20 @@ export const HeaderAuth = (props: Props) => {
</div> </div>
</a> </a>
</div> </div>
}
>
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
<button onClick={handleSaveClick}>
<span class={styles.textLabel}>{t('Save')}</span>
<Icon name="save" class={styles.icon} />
<Icon name="save" class={clsx(styles.icon, styles.iconHover)} />
</button>
</div>
</Show> </Show>
</Show> </Show>
</div> </div>
<Show when={isAuthenticated()}> <Show when={author()?.id}>
<ProfilePopup <ProfilePopup
onVisibilityChange={(isVisible) => { onVisibilityChange={(isVisible) => {
props.setIsProfilePopupVisible(isVisible) props.setIsProfilePopupVisible(isVisible)

View File

@ -46,7 +46,7 @@ const isEarlier = (date: Date) => {
export const NotificationsPanel = (props: Props) => { export const NotificationsPanel = (props: Props) => {
const [isLoading, setIsLoading] = createSignal(false) const [isLoading, setIsLoading] = createSignal(false)
const { isAuthenticated } = useSession() const { author } = useSession()
const { t } = useLocalize() const { t } = useLocalize()
const { const {
after, after,
@ -150,16 +150,13 @@ export const NotificationsPanel = (props: Props) => {
}) })
createEffect( createEffect(
on( on(author, async (a) => {
() => isAuthenticated(), if (a?.id) {
async () => {
if (isAuthenticated()) {
setIsLoading(true) setIsLoading(true)
await loadNextPage() await loadNextPage()
setIsLoading(false) setIsLoading(false)
} }
}, }),
),
) )
return ( return (

View File

@ -49,11 +49,9 @@ export const TopicCard = (props: TopicProps) => {
const handleFollowClick = () => { const handleFollowClick = () => {
requireAuthentication(() => { requireAuthentication(() => {
if (isSubscribed()) { isSubscribed()
unfollow(FollowingEntity.Topic, props.topic.slug) ? unfollow(FollowingEntity.Topic, props.topic.slug)
} else { : follow(FollowingEntity.Topic, props.topic.slug)
follow(FollowingEntity.Topic, props.topic.slug)
}
}, 'subscribe') }, 'subscribe')
} }

View File

@ -42,7 +42,6 @@ export const AuthorView = (props: Props) => {
const { followers: myFollowers } = useFollowing() const { followers: myFollowers } = useFollowing()
const { session } = useSession() const { session } = useSession()
const { sortedArticles } = useArticlesStore({ shouts: props.shouts }) const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
// const { authorEntities } = useAuthorsStore({ authors: [props.author] })
const { page: getPage, searchParams } = useRouter() const { page: getPage, searchParams } = useRouter()
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const [isBioExpanded, setIsBioExpanded] = createSignal(false) const [isBioExpanded, setIsBioExpanded] = createSignal(false)
@ -87,7 +86,7 @@ export const AuthorView = (props: Props) => {
setFollowing([...(authors || []), ...(topics || [])]) setFollowing([...(authors || []), ...(topics || [])])
setFollowers(followersResult || []) setFollowers(followersResult || [])
console.info('[components.Author] data loaded') console.debug('[components.Author] following data loaded', subscriptionsResult)
} catch (error) { } catch (error) {
console.error('[components.Author] fetch error', error) console.error('[components.Author] fetch error', error)
} }

View File

@ -1,6 +1,6 @@
import { openPage } from '@nanostores/router' import { openPage } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Show, createEffect, createSignal } from 'solid-js' import { For, Show, createEffect, createSignal, on } from 'solid-js'
import { useEditorContext } from '../../../context/editor' import { useEditorContext } from '../../../context/editor'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
@ -9,22 +9,24 @@ import { Shout } from '../../../graphql/schema/core.gen'
import { router } from '../../../stores/router' import { router } from '../../../stores/router'
import { Draft } from '../../Draft' import { Draft } from '../../Draft'
import { Loading } from '../../_shared/Loading'
import styles from './DraftsView.module.scss' import styles from './DraftsView.module.scss'
export const DraftsView = () => { export const DraftsView = () => {
const { isAuthenticated, isSessionLoaded } = useSession() const { session } = useSession()
const [drafts, setDrafts] = createSignal<Shout[]>([]) const [drafts, setDrafts] = createSignal<Shout[]>([])
const loadDrafts = async () => { createEffect(
if (apiClient.private) { on(
() => session(),
async (s) => {
if (s) {
const loadedDrafts = await apiClient.getDrafts() const loadedDrafts = await apiClient.getDrafts()
setDrafts(loadedDrafts.reverse() || []) setDrafts(loadedDrafts.reverse() || [])
} }
} },
),
createEffect(() => { )
if (isSessionLoaded()) loadDrafts()
})
const { publishShoutById, deleteShout } = useEditorContext() const { publishShoutById, deleteShout } = useEditorContext()
@ -44,11 +46,10 @@ export const DraftsView = () => {
return ( return (
<div class={clsx(styles.DraftsView)}> <div class={clsx(styles.DraftsView)}>
<Show when={isSessionLoaded()}> <Show when={session()?.user?.id} fallback={<Loading />}>
<div class="wide-container"> <div class="wide-container">
<div class="row"> <div class="row">
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5"> <div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">
<Show when={isAuthenticated()} fallback="Давайте авторизуемся">
<For each={drafts()}> <For each={drafts()}>
{(draft) => ( {(draft) => (
<Draft <Draft
@ -59,7 +60,6 @@ export const DraftsView = () => {
/> />
)} )}
</For> </For>
</Show>
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,6 +2,7 @@ import { clsx } from 'clsx'
import deepEqual from 'fast-deep-equal' import deepEqual from 'fast-deep-equal'
import { Accessor, Show, createMemo, createSignal, lazy, onCleanup, onMount } from 'solid-js' import { Accessor, Show, createMemo, createSignal, lazy, onCleanup, onMount } from 'solid-js'
import { createStore } from 'solid-js/store' import { createStore } from 'solid-js/store'
import { throttle } from 'throttle-debounce'
import { ShoutForm, useEditorContext } from '../../../context/editor' import { ShoutForm, useEditorContext } from '../../../context/editor'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
@ -41,7 +42,9 @@ export const EMPTY_TOPIC: Topic = {
slug: '', slug: '',
} }
const THROTTLING_INTERVAL = 2000
const AUTO_SAVE_INTERVAL = 5000 const AUTO_SAVE_INTERVAL = 5000
const AUTO_SAVE_DELAY = 5000
const handleScrollTopButtonClick = (e) => { const handleScrollTopButtonClick = (e) => {
e.preventDefault() e.preventDefault()
window.scrollTo({ window.scrollTo({
@ -65,12 +68,14 @@ export const EditView = (props: Props) => {
} = useEditorContext() } = useEditorContext()
const shoutTopics = props.shout.topics || [] const shoutTopics = props.shout.topics || []
// TODO: проверить сохранение черновика в local storage (не работает)
const draft = getDraftFromLocalStorage(props.shout.id) const draft = getDraftFromLocalStorage(props.shout.id)
if (draft) { if (draft) {
setForm(Object.keys(draft).length !== 0 ? draft : { shoutId: props.shout.id }) const draftForm = Object.keys(draft).length !== 0 ? draft : { shoutId: props.shout.id }
setForm(draftForm)
console.debug('draft from localstorage: ', draftForm)
} else { } else {
setForm({ const draftForm = {
slug: props.shout.slug, slug: props.shout.slug,
shoutId: props.shout.id, shoutId: props.shout.id,
title: props.shout.title, title: props.shout.title,
@ -83,7 +88,9 @@ export const EditView = (props: Props) => {
coverImageUrl: props.shout.cover, coverImageUrl: props.shout.cover,
media: props.shout.media, media: props.shout.media,
layout: props.shout.layout, layout: props.shout.layout,
}) }
setForm(draftForm)
console.debug('draft from props data: ', draftForm)
} }
const subtitleInput: { current: HTMLTextAreaElement } = { current: null } const subtitleInput: { current: HTMLTextAreaElement } = { current: null }
@ -106,9 +113,6 @@ export const EditView = (props: Props) => {
onCleanup(() => { onCleanup(() => {
window.removeEventListener('scroll', handleScroll) window.removeEventListener('scroll', handleScroll)
}) })
})
onMount(() => {
// eslint-disable-next-line unicorn/consistent-function-scoping // eslint-disable-next-line unicorn/consistent-function-scoping
const handleBeforeUnload = (event) => { const handleBeforeUnload = (event) => {
if (!deepEqual(prevForm, form)) { if (!deepEqual(prevForm, form)) {
@ -180,42 +184,39 @@ export const EditView = (props: Props) => {
let autoSaveTimeOutId: number | string | NodeJS.Timeout let autoSaveTimeOutId: number | string | NodeJS.Timeout
//TODO: add throttle const autoSave = async () => {
const autoSaveRecursive = () => {
autoSaveTimeOutId = setTimeout(async () => {
const hasChanges = !deepEqual(form, prevForm) const hasChanges = !deepEqual(form, prevForm)
if (hasChanges) { const hasTopic = Boolean(form.mainTopic)
if (hasChanges || hasTopic) {
console.debug('saving draft', form)
setSaving(true) setSaving(true)
if (props.shout?.published_at) {
saveDraftToLocalStorage(form) saveDraftToLocalStorage(form)
} else {
await saveDraft(form) await saveDraft(form)
}
setPrevForm(clone(form)) setPrevForm(clone(form))
setTimeout(() => { setTimeout(() => setSaving(false), AUTO_SAVE_DELAY)
setSaving(false)
}, 2000)
} }
}
// Throttle the autoSave function
const throttledAutoSave = throttle(THROTTLING_INTERVAL, autoSave)
const autoSaveRecursive = () => {
autoSaveTimeOutId = setTimeout(() => {
throttledAutoSave()
autoSaveRecursive() autoSaveRecursive()
}, AUTO_SAVE_INTERVAL) }, AUTO_SAVE_INTERVAL)
} }
const stopAutoSave = () => {
clearTimeout(autoSaveTimeOutId)
}
onMount(() => { onMount(() => {
autoSaveRecursive() autoSaveRecursive()
}) onCleanup(() => clearTimeout(autoSaveTimeOutId))
onCleanup(() => {
stopAutoSave()
}) })
const showSubtitleInput = () => { const showSubtitleInput = () => {
setIsSubtitleVisible(true) setIsSubtitleVisible(true)
subtitleInput.current.focus() subtitleInput.current.focus()
} }
const showLeadInput = () => { const showLeadInput = () => {
setIsLeadVisible(true) setIsLeadVisible(true)
} }

View File

@ -49,7 +49,7 @@ type VisibilityItem = {
} }
type FeedSearchParams = { type FeedSearchParams = {
by: 'publish_date' | 'likes' | 'comments' by: 'publish_date' | 'likes' | 'last_comment'
period: FeedPeriod period: FeedPeriod
visibility: VisibilityMode visibility: VisibilityMode
} }
@ -258,10 +258,10 @@ export const FeedView = (props: Props) => {
</li> </li>
<li <li
class={clsx({ class={clsx({
'view-switcher__item--selected': searchParams().by === 'comments', 'view-switcher__item--selected': searchParams().by === 'last_comment',
})} })}
> >
<span class="link" onClick={() => changeSearchParams({ by: 'comments' })}> <span class="link" onClick={() => changeSearchParams({ by: 'last_comment' })}>
{t('Most commented')} {t('Most commented')}
</span> </span>
</li> </li>

View File

@ -40,11 +40,11 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0) const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0)
const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0) const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0)
const [notificationEntities, setNotificationEntities] = createStore<Record<string, NotificationGroup>>({}) const [notificationEntities, setNotificationEntities] = createStore<Record<string, NotificationGroup>>({})
const { isAuthenticated } = useSession() const { author } = useSession()
const { addHandler } = useConnect() const { addHandler } = useConnect()
const loadNotificationsGrouped = async (options: { after: number; limit?: number; offset?: number }) => { const loadNotificationsGrouped = async (options: { after: number; limit?: number; offset?: number }) => {
if (isAuthenticated() && notifierClient?.private) { if (author()?.id && notifierClient?.private) {
const notificationsResult = await notifierClient.getNotifications(options) const notificationsResult = await notifierClient.getNotifications(options)
const groups = notificationsResult?.notifications || [] const groups = notificationsResult?.notifications || []
const total = notificationsResult?.total || 0 const total = notificationsResult?.total || 0
@ -74,7 +74,7 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
onMount(() => { onMount(() => {
addHandler((data: SSEMessage) => { addHandler((data: SSEMessage) => {
if (data.entity === 'reaction' && isAuthenticated()) { if (data.entity === 'reaction' && author()?.id) {
console.info('[context.notifications] event', data) console.info('[context.notifications] event', data)
loadNotificationsGrouped({ after: after(), limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) }) loadNotificationsGrouped({ after: after(), limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) })
} }
@ -91,14 +91,14 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
} }
const markSeenAll = async () => { const markSeenAll = async () => {
if (isAuthenticated() && notifierClient.private) { if (author()?.id && notifierClient.private) {
await notifierClient.markSeenAfter({ after: after() }) await notifierClient.markSeenAfter({ after: after() })
await loadNotificationsGrouped({ after: after(), limit: loadedNotificationsCount() }) await loadNotificationsGrouped({ after: after(), limit: loadedNotificationsCount() })
} }
} }
const markSeen = async (notification_id: number) => { const markSeen = async (notification_id: number) => {
if (isAuthenticated() && notifierClient.private) { if (author()?.id && notifierClient.private) {
await notifierClient.markSeen(notification_id) await notifierClient.markSeen(notification_id)
await loadNotificationsGrouped({ after: after(), limit: loadedNotificationsCount() }) await loadNotificationsGrouped({ after: after(), limit: loadedNotificationsCount() })
} }

View File

@ -48,7 +48,6 @@ export type SessionContextType = {
author: Resource<Author | null> author: Resource<Author | null>
authError: Accessor<string> authError: Accessor<string>
isSessionLoaded: Accessor<boolean> isSessionLoaded: Accessor<boolean>
isAuthenticated: Accessor<boolean>
loadSession: () => AuthToken | Promise<AuthToken> loadSession: () => AuthToken | Promise<AuthToken>
setSession: (token: AuthToken | null) => void // setSession setSession: (token: AuthToken | null) => void // setSession
loadAuthor: (info?: unknown) => Author | Promise<Author> loadAuthor: (info?: unknown) => Author | Promise<Author>
@ -271,12 +270,9 @@ export const SessionProvider = (props: {
// callback state updater // callback state updater
createEffect( createEffect(
on( on([() => props.onStateChangeCallback, session], ([_, ses]) => {
() => props.onStateChangeCallback, ses?.user?.id && props.onStateChangeCallback(ses)
() => { }),
props.onStateChangeCallback(session())
},
),
) )
const [authCallback, setAuthCallback] = createSignal<() => void>(noop) const [authCallback, setAuthCallback] = createSignal<() => void>(noop)
@ -378,9 +374,6 @@ export const SessionProvider = (props: {
console.warn(error) console.warn(error)
} }
} }
const isAuthenticated = createMemo(() => Boolean(author()))
const actions = { const actions = {
loadSession, loadSession,
requireAuthentication, requireAuthentication,
@ -405,7 +398,6 @@ export const SessionProvider = (props: {
isSessionLoaded, isSessionLoaded,
author, author,
...actions, ...actions,
isAuthenticated,
resendVerifyEmail, resendVerifyEmail,
} }

View File

@ -1,9 +1,10 @@
import { Show, Suspense, createMemo, createSignal, lazy, onMount } from 'solid-js' import { Show, Suspense, createEffect, createMemo, createSignal, lazy, on, onMount } 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'
import { PageLayout } from '../components/_shared/PageLayout' import { PageLayout } from '../components/_shared/PageLayout'
import { useLocalize } from '../context/localize' import { useLocalize } from '../context/localize'
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 } from '../stores/router'
@ -14,67 +15,70 @@ import { LayoutType } from './types'
const EditView = lazy(() => import('../components/Views/EditView/EditView')) const EditView = lazy(() => import('../components/Views/EditView/EditView'))
export const EditPage = () => { const getContentTypeTitle = (layout: LayoutType) => {
const snackbar = useSnackbar() switch (layout) {
const { t } = useLocalize() case 'audio':
return 'Publish Album'
case 'image':
return 'Create gallery'
case 'video':
return 'Create video'
case 'literature':
return 'New literary work'
default:
return 'Write an article'
}
}
const [shout, setShout] = createSignal<Shout>(null) export const EditPage = () => {
const loadMyShout = async (shout_id: number) => { const { t } = useLocalize()
if (shout_id) { const { session } = useSession()
const { shout: loadedShout, error } = await apiClient.getMyShout(shout_id) const snackbar = useSnackbar()
console.log(loadedShout)
if (error) { const fail = async (error: string) => {
await snackbar?.showSnackbar({ type: 'error', body: t('This content is not published yet') }) console.error(error)
await snackbar?.showSnackbar({ type: 'error', body: t(error) })
redirectPage(router, 'drafts') redirectPage(router, 'drafts')
}
const [shoutId, setShoutId] = createSignal<number>(0)
const [shout, setShout] = createSignal<Shout>()
onMount(() => {
const shoutId = window.location.pathname.split('/').pop()
const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10)
console.debug(`editing shout ${shoutIdFromUrl}`)
if (shoutIdFromUrl) setShoutId(shoutIdFromUrl)
})
createEffect(
on([session, shout, shoutId], async ([ses, sh, shid]) => {
if (ses?.user && !sh && shid) {
const { shout: loadedShout, error } = await apiClient.getMyShout(shid)
if (error) {
fail(error)
} else { } else {
setShout(loadedShout) setShout(loadedShout)
} }
} }
} }),
)
onMount(async () => {
const shout_id = window.location.pathname.split('/').pop()
if (shout_id) {
try {
await loadMyShout(Number.parseInt(shout_id, 10))
} catch (e) {
console.error(e)
}
}
})
const title = createMemo(() => { const title = createMemo(() => {
if (!shout()) { if (!shout()) {
return t('Create post') return t('Create post')
} }
return t(getContentTypeTitle(shout()?.layout as LayoutType))
switch (shout().layout as LayoutType) {
case 'audio': {
return t('Publish Album')
}
case 'image': {
return t('Create gallery')
}
case 'video': {
return t('Create video')
}
case 'literature': {
return t('New literary work')
}
default: {
return t('Write an article')
}
}
}) })
return ( return (
<PageLayout title={title()}> <PageLayout title={title()}>
<AuthGuard> <AuthGuard>
<Show when={shout()}>
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
<EditView shout={shout()} /> <Show when={shout()} fallback={<Loading />}>
</Suspense> <EditView shout={shout() as Shout} />
</Show> </Show>
</Suspense>
</AuthGuard> </AuthGuard>
</PageLayout> </PageLayout>
) )

View File

@ -69,6 +69,7 @@ const checkOpenOnClient = (link: HTMLAnchorElement, event) => {
) )
} }
// TODO: use scrollToHash or remove
const _scrollToHash = (hash: string) => { const _scrollToHash = (hash: string) => {
let selector = hash let selector = hash

View File

@ -6,7 +6,7 @@ import { Author, QueryLoad_Authors_ByArgs } from '../../graphql/schema/core.gen'
export type AuthorsSortBy = 'shouts' | 'name' | 'followers' export type AuthorsSortBy = 'shouts' | 'name' | 'followers'
type SortedAuthorsSetter = (prev: Author[]) => Author[] type SortedAuthorsSetter = (prev: Author[]) => Author[]
// FIXME: use signal or remove
const [_sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('name') const [_sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('name')
export const setAuthorsSort = (sortBy: AuthorsSortBy) => setSortAllBy(sortBy) export const setAuthorsSort = (sortBy: AuthorsSortBy) => setSortAllBy(sortBy)

View File

@ -2,6 +2,7 @@ import { RANDOM_TOPICS_COUNT } from '../components/Views/Home'
import { Topic } from '../graphql/schema/core.gen' import { Topic } from '../graphql/schema/core.gen'
export const getRandomTopicsFromArray = (topics: Topic[], count: number = RANDOM_TOPICS_COUNT): Topic[] => { export const getRandomTopicsFromArray = (topics: Topic[], count: number = RANDOM_TOPICS_COUNT): Topic[] => {
if (!Array.isArray(topics)) return []
const shuffledTopics = [...topics].sort(() => 0.5 - Math.random()) const shuffledTopics = [...topics].sort(() => 0.5 - Math.random())
return shuffledTopics.slice(0, count) return shuffledTopics.slice(0, count)
} }