Merge pull request #447 from Discours/hotfix/editor-permission
use-session in editor
This commit is contained in:
commit
8f09d6fc54
|
@ -99,4 +99,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -40,16 +40,12 @@ import { InboxPage } from '../pages/inbox.page'
|
|||
import { HomePage } from '../pages/index.page'
|
||||
import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
|
||||
import { ProfileSettingsPage } from '../pages/profile/profileSettings.page'
|
||||
//TODO: ProfileSubscriptionsPage - garbage code?
|
||||
import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page'
|
||||
import { SearchPage } from '../pages/search.page'
|
||||
import { TopicPage } from '../pages/topic.page'
|
||||
import { ROUTES, useRouter } from '../stores/router'
|
||||
import { MODALS, showModal } from '../stores/ui'
|
||||
|
||||
// TODO: lazy load
|
||||
// const SomePage = lazy(() => import('./Pages/SomePage'))
|
||||
|
||||
const pagesMap: Record<keyof typeof ROUTES, Component<PageProps>> = {
|
||||
author: AuthorPage,
|
||||
authorComments: AuthorPage,
|
||||
|
|
|
@ -75,7 +75,7 @@ export const FullArticle = (props: Props) => {
|
|||
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
|
||||
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
|
||||
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)))
|
||||
|
||||
|
@ -561,7 +561,7 @@ export const FullArticle = (props: Props) => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<Show when={isAuthenticated() && !canEdit()}>
|
||||
<Show when={author()?.id && !canEdit()}>
|
||||
<div class={styles.help}>
|
||||
<button class="button">{t('Cooperate')}</button>
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,7 @@ type Props = {
|
|||
}
|
||||
|
||||
export const AuthGuard = (props: Props) => {
|
||||
const { isAuthenticated, isSessionLoaded } = useSession()
|
||||
const { author, isSessionLoaded } = useSession()
|
||||
const { changeSearchParams } = useRouter<RootSearchParams & AuthModalSearchParams>()
|
||||
|
||||
createEffect(() => {
|
||||
|
@ -20,7 +20,7 @@ export const AuthGuard = (props: Props) => {
|
|||
return
|
||||
}
|
||||
if (isSessionLoaded()) {
|
||||
if (isAuthenticated()) {
|
||||
if (author()?.id) {
|
||||
hideModal()
|
||||
} else {
|
||||
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>
|
||||
}
|
||||
|
|
|
@ -119,6 +119,9 @@ export const AuthorBadge = (props: Props) => {
|
|||
<Show when={props.author?.stat.shouts > 0}>
|
||||
<div>{t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}</div>
|
||||
</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}>
|
||||
<div>{t('FollowersWithCount', { count: props.author.stat?.followers ?? 0 })}</div>
|
||||
</Show>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useLocalize } from '../../context/localize'
|
||||
import { useRouter } from '../../stores/router'
|
||||
import { showModal } from '../../stores/ui'
|
||||
import { AuthModalSearchParams } from '../Nav/AuthModal/types'
|
||||
import type { AuthModalSearchParams } from '../Nav/AuthModal/types'
|
||||
|
||||
import styles from './Hero.module.scss'
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { composeMediaItems } from '../../../utils/composeMediaItems'
|
|||
import { AudioPlayer } from '../../Article/AudioPlayer'
|
||||
import { DropArea } from '../../_shared/DropArea'
|
||||
|
||||
// import { Buffer } from 'node:buffer'
|
||||
import styles from './AudioUploader.module.scss'
|
||||
|
||||
window.Buffer = Buffer
|
||||
|
|
|
@ -30,8 +30,10 @@ const embedData = (data) => {
|
|||
|
||||
// biome-ignore lint/style/useForOf: <explanation>
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
const attribute = attributes[i]
|
||||
result[attribute.name] = attribute.value
|
||||
const attribute = attributes.item(i)
|
||||
if (attribute) {
|
||||
result[attribute.name] = attribute.value
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
|
|
|
@ -23,8 +23,16 @@ type Props = {
|
|||
|
||||
export const Panel = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
const { isEditorPanelVisible, wordCounter, editorRef, form, toggleEditorPanel, saveShout, publishShout } =
|
||||
useEditorContext()
|
||||
const {
|
||||
isEditorPanelVisible,
|
||||
wordCounter,
|
||||
editorRef,
|
||||
form,
|
||||
toggleEditorPanel,
|
||||
saveShout,
|
||||
saveDraft,
|
||||
publishShout,
|
||||
} = useEditorContext()
|
||||
|
||||
const containerRef: { current: HTMLElement } = { current: null }
|
||||
const [isShortcutsVisible, setIsShortcutsVisible] = createSignal(false)
|
||||
|
@ -43,7 +51,12 @@ export const Panel = (props: Props) => {
|
|||
})
|
||||
|
||||
const handleSaveClick = () => {
|
||||
saveShout(form)
|
||||
const hasTopics = form.selectedTopics?.length > 0
|
||||
if (hasTopics) {
|
||||
saveShout(form)
|
||||
} else {
|
||||
saveDraft(form)
|
||||
}
|
||||
}
|
||||
|
||||
const html = useEditorHTML(() => editorRef.current())
|
||||
|
|
|
@ -31,7 +31,7 @@ export const LoginForm = () => {
|
|||
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
||||
const [password, setPassword] = createSignal('')
|
||||
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
||||
|
||||
// FIXME: use signal or remove
|
||||
const [_isLinkSent, setIsLinkSent] = createSignal(false)
|
||||
const authFormRef: { current: HTMLFormElement } = { current: null }
|
||||
const { showSnackbar } = useSnackbar()
|
||||
|
|
|
@ -32,6 +32,7 @@ export const RegisterForm = () => {
|
|||
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
|
||||
const { t } = useLocalize()
|
||||
const { signUp, isRegistered, resendVerifyEmail } = useSession()
|
||||
// FIXME: use submit error data or remove signal
|
||||
const [_submitError, setSubmitError] = createSignal('')
|
||||
const [fullName, setFullName] = createSignal('')
|
||||
const [password, setPassword] = createSignal('')
|
||||
|
|
|
@ -59,7 +59,7 @@ export const Header = (props: Props) => {
|
|||
const [isTopicsVisible, setIsTopicsVisible] = createSignal(false)
|
||||
const [isZineVisible, setIsZineVisible] = createSignal(false)
|
||||
const [isFeedVisible, setIsFeedVisible] = createSignal(false)
|
||||
const { isAuthenticated } = useSession()
|
||||
const { session } = useSession()
|
||||
|
||||
const toggleFixed = () => setFixed(!fixed())
|
||||
|
||||
|
@ -335,7 +335,7 @@ export const Header = (props: Props) => {
|
|||
<Show when={props.title}>
|
||||
<div
|
||||
class={clsx(styles.articleControls, 'col-auto', {
|
||||
[styles.articleControlsAuthorized]: isAuthenticated(),
|
||||
[styles.articleControlsAuthorized]: session()?.user?.id,
|
||||
})}
|
||||
>
|
||||
<SharePopup
|
||||
|
|
|
@ -32,14 +32,14 @@ const MD_WIDTH_BREAKPOINT = 992
|
|||
export const HeaderAuth = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
const { page } = useRouter()
|
||||
const { session, author, isAuthenticated, isSessionLoaded } = useSession()
|
||||
const { session, author, isSessionLoaded } = useSession()
|
||||
const { unreadNotificationsCount, showNotificationsPanel } = useNotifications()
|
||||
const { form, toggleEditorPanel, saveShout, publishShout } = useEditorContext()
|
||||
const { form, toggleEditorPanel, saveShout, saveDraft, publishShout } = useEditorContext()
|
||||
|
||||
const handleBellIconClick = (event: Event) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (!isAuthenticated()) {
|
||||
if (!author()?.id) {
|
||||
showModal('auth')
|
||||
return
|
||||
}
|
||||
|
@ -48,19 +48,22 @@ export const HeaderAuth = (props: Props) => {
|
|||
}
|
||||
|
||||
const isEditorPage = createMemo(() => page().route === 'edit' || page().route === 'editSettings')
|
||||
const isNotificationsVisible = createMemo(() => isAuthenticated() && !isEditorPage())
|
||||
const isSaveButtonVisible = createMemo(() => isAuthenticated() && isEditorPage())
|
||||
const isNotificationsVisible = createMemo(() => author()?.id && !isEditorPage())
|
||||
const isSaveButtonVisible = createMemo(() => author()?.id && isEditorPage())
|
||||
const isCreatePostButtonVisible = createMemo(() => !isEditorPage())
|
||||
const isAuthenticatedControlsVisible = createMemo(
|
||||
() => isAuthenticated() && session()?.user?.email_verified,
|
||||
)
|
||||
const isAuthenticatedControlsVisible = createMemo(() => author()?.id && session()?.user?.email_verified)
|
||||
|
||||
const handleBurgerButtonClick = () => {
|
||||
toggleEditorPanel()
|
||||
}
|
||||
|
||||
const _handleSaveButtonClick = () => {
|
||||
saveShout(form)
|
||||
const handleSaveClick = () => {
|
||||
const hasTopics = form.selectedTopics?.length > 0
|
||||
if (hasTopics) {
|
||||
saveShout(form)
|
||||
} else {
|
||||
saveDraft(form)
|
||||
}
|
||||
}
|
||||
|
||||
const [width, setWidth] = createSignal(0)
|
||||
|
@ -106,14 +109,8 @@ export const HeaderAuth = (props: Props) => {
|
|||
<Show when={isSessionLoaded()} keyed={true}>
|
||||
<div class={clsx('col-auto col-lg-7', styles.usernav)}>
|
||||
<div class={styles.userControl}>
|
||||
<Show when={isCreatePostButtonVisible() && isAuthenticated()}>
|
||||
<div
|
||||
class={clsx(
|
||||
styles.userControlItem,
|
||||
styles.userControlItemVerbose,
|
||||
styles.userControlItemCreate,
|
||||
)}
|
||||
>
|
||||
<Show when={isCreatePostButtonVisible() && author()?.id}>
|
||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||
<a href={getPagePath(router, 'create')}>
|
||||
<span class={styles.textLabel}>{t('Create post')}</span>
|
||||
<Icon name="pencil-outline" class={styles.icon} />
|
||||
|
@ -220,14 +217,8 @@ export const HeaderAuth = (props: Props) => {
|
|||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={isCreatePostButtonVisible() && !isAuthenticated()}>
|
||||
<div
|
||||
class={clsx(
|
||||
styles.userControlItem,
|
||||
styles.userControlItemVerbose,
|
||||
styles.userControlItemCreate,
|
||||
)}
|
||||
>
|
||||
<Show when={isCreatePostButtonVisible() && !author()?.id}>
|
||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||
<a href={getPagePath(router, 'create')}>
|
||||
<span class={styles.textLabel}>{t('Create post')}</span>
|
||||
<Icon name="pencil-outline" class={styles.icon} />
|
||||
|
@ -239,7 +230,7 @@ export const HeaderAuth = (props: Props) => {
|
|||
<Show
|
||||
when={isAuthenticatedControlsVisible()}
|
||||
fallback={
|
||||
<Show when={!isAuthenticated()}>
|
||||
<Show when={!author()?.id}>
|
||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
|
||||
<a href="?m=auth&mode=login">
|
||||
<span class={styles.textLabel}>{t('Enter')}</span>
|
||||
|
@ -250,25 +241,31 @@ export const HeaderAuth = (props: Props) => {
|
|||
</Show>
|
||||
}
|
||||
>
|
||||
<Show when={!isSaveButtonVisible()}>
|
||||
<div
|
||||
class={clsx(
|
||||
styles.userControlItem,
|
||||
// styles.userControlItemInbox
|
||||
)}
|
||||
>
|
||||
<a href={getPagePath(router, 'inbox')}>
|
||||
<div classList={{ entered: page().path === '/inbox' }}>
|
||||
<Icon name="inbox-white" class={styles.icon} />
|
||||
<Icon name="inbox-white-hover" class={clsx(styles.icon, styles.iconHover)} />
|
||||
</div>
|
||||
</a>
|
||||
<Show
|
||||
when={isSaveButtonVisible()}
|
||||
fallback={
|
||||
<div class={clsx(styles.userControlItem)}>
|
||||
<a href={getPagePath(router, 'inbox')}>
|
||||
<div classList={{ entered: page().path === '/inbox' }}>
|
||||
<Icon name="inbox-white" class={styles.icon} />
|
||||
<Icon name="inbox-white-hover" class={clsx(styles.icon, styles.iconHover)} />
|
||||
</div>
|
||||
</a>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<Show when={isAuthenticated()}>
|
||||
<Show when={author()?.id}>
|
||||
<ProfilePopup
|
||||
onVisibilityChange={(isVisible) => {
|
||||
props.setIsProfilePopupVisible(isVisible)
|
||||
|
|
|
@ -46,7 +46,7 @@ const isEarlier = (date: Date) => {
|
|||
|
||||
export const NotificationsPanel = (props: Props) => {
|
||||
const [isLoading, setIsLoading] = createSignal(false)
|
||||
const { isAuthenticated } = useSession()
|
||||
const { author } = useSession()
|
||||
const { t } = useLocalize()
|
||||
const {
|
||||
after,
|
||||
|
@ -150,16 +150,13 @@ export const NotificationsPanel = (props: Props) => {
|
|||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => isAuthenticated(),
|
||||
async () => {
|
||||
if (isAuthenticated()) {
|
||||
setIsLoading(true)
|
||||
await loadNextPage()
|
||||
setIsLoading(false)
|
||||
}
|
||||
},
|
||||
),
|
||||
on(author, async (a) => {
|
||||
if (a?.id) {
|
||||
setIsLoading(true)
|
||||
await loadNextPage()
|
||||
setIsLoading(false)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
@ -49,11 +49,9 @@ export const TopicCard = (props: TopicProps) => {
|
|||
|
||||
const handleFollowClick = () => {
|
||||
requireAuthentication(() => {
|
||||
if (isSubscribed()) {
|
||||
unfollow(FollowingEntity.Topic, props.topic.slug)
|
||||
} else {
|
||||
follow(FollowingEntity.Topic, props.topic.slug)
|
||||
}
|
||||
isSubscribed()
|
||||
? unfollow(FollowingEntity.Topic, props.topic.slug)
|
||||
: follow(FollowingEntity.Topic, props.topic.slug)
|
||||
}, 'subscribe')
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,6 @@ export const AuthorView = (props: Props) => {
|
|||
const { followers: myFollowers } = useFollowing()
|
||||
const { session } = useSession()
|
||||
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
||||
// const { authorEntities } = useAuthorsStore({ authors: [props.author] })
|
||||
const { page: getPage, searchParams } = useRouter()
|
||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||
const [isBioExpanded, setIsBioExpanded] = createSignal(false)
|
||||
|
@ -87,7 +86,7 @@ export const AuthorView = (props: Props) => {
|
|||
setFollowing([...(authors || []), ...(topics || [])])
|
||||
setFollowers(followersResult || [])
|
||||
|
||||
console.info('[components.Author] data loaded')
|
||||
console.debug('[components.Author] following data loaded', subscriptionsResult)
|
||||
} catch (error) {
|
||||
console.error('[components.Author] fetch error', error)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { openPage } from '@nanostores/router'
|
||||
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 { useSession } from '../../../context/session'
|
||||
|
@ -9,22 +9,24 @@ import { Shout } from '../../../graphql/schema/core.gen'
|
|||
import { router } from '../../../stores/router'
|
||||
import { Draft } from '../../Draft'
|
||||
|
||||
import { Loading } from '../../_shared/Loading'
|
||||
import styles from './DraftsView.module.scss'
|
||||
|
||||
export const DraftsView = () => {
|
||||
const { isAuthenticated, isSessionLoaded } = useSession()
|
||||
const { session } = useSession()
|
||||
const [drafts, setDrafts] = createSignal<Shout[]>([])
|
||||
|
||||
const loadDrafts = async () => {
|
||||
if (apiClient.private) {
|
||||
const loadedDrafts = await apiClient.getDrafts()
|
||||
setDrafts(loadedDrafts.reverse() || [])
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (isSessionLoaded()) loadDrafts()
|
||||
})
|
||||
createEffect(
|
||||
on(
|
||||
() => session(),
|
||||
async (s) => {
|
||||
if (s) {
|
||||
const loadedDrafts = await apiClient.getDrafts()
|
||||
setDrafts(loadedDrafts.reverse() || [])
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const { publishShoutById, deleteShout } = useEditorContext()
|
||||
|
||||
|
@ -44,22 +46,20 @@ export const DraftsView = () => {
|
|||
|
||||
return (
|
||||
<div class={clsx(styles.DraftsView)}>
|
||||
<Show when={isSessionLoaded()}>
|
||||
<Show when={session()?.user?.id} fallback={<Loading />}>
|
||||
<div class="wide-container">
|
||||
<div class="row">
|
||||
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">
|
||||
<Show when={isAuthenticated()} fallback="Давайте авторизуемся">
|
||||
<For each={drafts()}>
|
||||
{(draft) => (
|
||||
<Draft
|
||||
class={styles.draft}
|
||||
shout={draft}
|
||||
onDelete={handleDraftDelete}
|
||||
onPublish={handleDraftPublish}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
<For each={drafts()}>
|
||||
{(draft) => (
|
||||
<Draft
|
||||
class={styles.draft}
|
||||
shout={draft}
|
||||
onDelete={handleDraftDelete}
|
||||
onPublish={handleDraftPublish}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,6 +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 { ShoutForm, useEditorContext } from '../../../context/editor'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
|
@ -41,7 +42,9 @@ export const EMPTY_TOPIC: Topic = {
|
|||
slug: '',
|
||||
}
|
||||
|
||||
const THROTTLING_INTERVAL = 2000
|
||||
const AUTO_SAVE_INTERVAL = 5000
|
||||
const AUTO_SAVE_DELAY = 5000
|
||||
const handleScrollTopButtonClick = (e) => {
|
||||
e.preventDefault()
|
||||
window.scrollTo({
|
||||
|
@ -65,12 +68,14 @@ export const EditView = (props: Props) => {
|
|||
} = useEditorContext()
|
||||
const shoutTopics = props.shout.topics || []
|
||||
|
||||
// TODO: проверить сохранение черновика в local storage (не работает)
|
||||
const draft = getDraftFromLocalStorage(props.shout.id)
|
||||
|
||||
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 {
|
||||
setForm({
|
||||
const draftForm = {
|
||||
slug: props.shout.slug,
|
||||
shoutId: props.shout.id,
|
||||
title: props.shout.title,
|
||||
|
@ -83,7 +88,9 @@ export const EditView = (props: Props) => {
|
|||
coverImageUrl: props.shout.cover,
|
||||
media: props.shout.media,
|
||||
layout: props.shout.layout,
|
||||
})
|
||||
}
|
||||
setForm(draftForm)
|
||||
console.debug('draft from props data: ', draftForm)
|
||||
}
|
||||
|
||||
const subtitleInput: { current: HTMLTextAreaElement } = { current: null }
|
||||
|
@ -106,9 +113,6 @@ export const EditView = (props: Props) => {
|
|||
onCleanup(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
const handleBeforeUnload = (event) => {
|
||||
if (!deepEqual(prevForm, form)) {
|
||||
|
@ -180,42 +184,39 @@ export const EditView = (props: Props) => {
|
|||
|
||||
let autoSaveTimeOutId: number | string | NodeJS.Timeout
|
||||
|
||||
//TODO: add throttle
|
||||
const autoSave = async () => {
|
||||
const hasChanges = !deepEqual(form, prevForm)
|
||||
const hasTopic = Boolean(form.mainTopic)
|
||||
if (hasChanges || hasTopic) {
|
||||
console.debug('saving draft', form)
|
||||
setSaving(true)
|
||||
saveDraftToLocalStorage(form)
|
||||
await saveDraft(form)
|
||||
setPrevForm(clone(form))
|
||||
setTimeout(() => setSaving(false), AUTO_SAVE_DELAY)
|
||||
}
|
||||
}
|
||||
|
||||
// Throttle the autoSave function
|
||||
const throttledAutoSave = throttle(THROTTLING_INTERVAL, autoSave)
|
||||
|
||||
const autoSaveRecursive = () => {
|
||||
autoSaveTimeOutId = setTimeout(async () => {
|
||||
const hasChanges = !deepEqual(form, prevForm)
|
||||
if (hasChanges) {
|
||||
setSaving(true)
|
||||
if (props.shout?.published_at) {
|
||||
saveDraftToLocalStorage(form)
|
||||
} else {
|
||||
await saveDraft(form)
|
||||
}
|
||||
setPrevForm(clone(form))
|
||||
setTimeout(() => {
|
||||
setSaving(false)
|
||||
}, 2000)
|
||||
}
|
||||
autoSaveTimeOutId = setTimeout(() => {
|
||||
throttledAutoSave()
|
||||
autoSaveRecursive()
|
||||
}, AUTO_SAVE_INTERVAL)
|
||||
}
|
||||
|
||||
const stopAutoSave = () => {
|
||||
clearTimeout(autoSaveTimeOutId)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
autoSaveRecursive()
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
stopAutoSave()
|
||||
onCleanup(() => clearTimeout(autoSaveTimeOutId))
|
||||
})
|
||||
|
||||
const showSubtitleInput = () => {
|
||||
setIsSubtitleVisible(true)
|
||||
subtitleInput.current.focus()
|
||||
}
|
||||
|
||||
const showLeadInput = () => {
|
||||
setIsLeadVisible(true)
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ type VisibilityItem = {
|
|||
}
|
||||
|
||||
type FeedSearchParams = {
|
||||
by: 'publish_date' | 'likes' | 'comments'
|
||||
by: 'publish_date' | 'likes' | 'last_comment'
|
||||
period: FeedPeriod
|
||||
visibility: VisibilityMode
|
||||
}
|
||||
|
@ -258,10 +258,10 @@ export const FeedView = (props: Props) => {
|
|||
</li>
|
||||
<li
|
||||
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')}
|
||||
</span>
|
||||
</li>
|
||||
|
|
|
@ -40,11 +40,11 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
|||
const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0)
|
||||
const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0)
|
||||
const [notificationEntities, setNotificationEntities] = createStore<Record<string, NotificationGroup>>({})
|
||||
const { isAuthenticated } = useSession()
|
||||
const { author } = useSession()
|
||||
const { addHandler } = useConnect()
|
||||
|
||||
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 groups = notificationsResult?.notifications || []
|
||||
const total = notificationsResult?.total || 0
|
||||
|
@ -74,7 +74,7 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
|||
|
||||
onMount(() => {
|
||||
addHandler((data: SSEMessage) => {
|
||||
if (data.entity === 'reaction' && isAuthenticated()) {
|
||||
if (data.entity === 'reaction' && author()?.id) {
|
||||
console.info('[context.notifications] event', data)
|
||||
loadNotificationsGrouped({ after: after(), limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) })
|
||||
}
|
||||
|
@ -91,14 +91,14 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
|||
}
|
||||
|
||||
const markSeenAll = async () => {
|
||||
if (isAuthenticated() && notifierClient.private) {
|
||||
if (author()?.id && notifierClient.private) {
|
||||
await notifierClient.markSeenAfter({ after: after() })
|
||||
await loadNotificationsGrouped({ after: after(), limit: loadedNotificationsCount() })
|
||||
}
|
||||
}
|
||||
|
||||
const markSeen = async (notification_id: number) => {
|
||||
if (isAuthenticated() && notifierClient.private) {
|
||||
if (author()?.id && notifierClient.private) {
|
||||
await notifierClient.markSeen(notification_id)
|
||||
await loadNotificationsGrouped({ after: after(), limit: loadedNotificationsCount() })
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ export type SessionContextType = {
|
|||
author: Resource<Author | null>
|
||||
authError: Accessor<string>
|
||||
isSessionLoaded: Accessor<boolean>
|
||||
isAuthenticated: Accessor<boolean>
|
||||
loadSession: () => AuthToken | Promise<AuthToken>
|
||||
setSession: (token: AuthToken | null) => void // setSession
|
||||
loadAuthor: (info?: unknown) => Author | Promise<Author>
|
||||
|
@ -271,12 +270,9 @@ export const SessionProvider = (props: {
|
|||
|
||||
// callback state updater
|
||||
createEffect(
|
||||
on(
|
||||
() => props.onStateChangeCallback,
|
||||
() => {
|
||||
props.onStateChangeCallback(session())
|
||||
},
|
||||
),
|
||||
on([() => props.onStateChangeCallback, session], ([_, ses]) => {
|
||||
ses?.user?.id && props.onStateChangeCallback(ses)
|
||||
}),
|
||||
)
|
||||
|
||||
const [authCallback, setAuthCallback] = createSignal<() => void>(noop)
|
||||
|
@ -378,9 +374,6 @@ export const SessionProvider = (props: {
|
|||
console.warn(error)
|
||||
}
|
||||
}
|
||||
|
||||
const isAuthenticated = createMemo(() => Boolean(author()))
|
||||
|
||||
const actions = {
|
||||
loadSession,
|
||||
requireAuthentication,
|
||||
|
@ -405,7 +398,6 @@ export const SessionProvider = (props: {
|
|||
isSessionLoaded,
|
||||
author,
|
||||
...actions,
|
||||
isAuthenticated,
|
||||
resendVerifyEmail,
|
||||
}
|
||||
|
||||
|
|
|
@ -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 { Loading } from '../components/_shared/Loading'
|
||||
import { PageLayout } from '../components/_shared/PageLayout'
|
||||
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'
|
||||
|
@ -14,67 +15,70 @@ import { LayoutType } from './types'
|
|||
|
||||
const EditView = lazy(() => import('../components/Views/EditView/EditView'))
|
||||
|
||||
export const EditPage = () => {
|
||||
const snackbar = useSnackbar()
|
||||
const { t } = useLocalize()
|
||||
const getContentTypeTitle = (layout: LayoutType) => {
|
||||
switch (layout) {
|
||||
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)
|
||||
const loadMyShout = async (shout_id: number) => {
|
||||
if (shout_id) {
|
||||
const { shout: loadedShout, error } = await apiClient.getMyShout(shout_id)
|
||||
console.log(loadedShout)
|
||||
if (error) {
|
||||
await snackbar?.showSnackbar({ type: 'error', body: t('This content is not published yet') })
|
||||
redirectPage(router, 'drafts')
|
||||
} else {
|
||||
setShout(loadedShout)
|
||||
}
|
||||
}
|
||||
export const EditPage = () => {
|
||||
const { t } = useLocalize()
|
||||
const { session } = useSession()
|
||||
const snackbar = useSnackbar()
|
||||
|
||||
const fail = async (error: string) => {
|
||||
console.error(error)
|
||||
await snackbar?.showSnackbar({ type: 'error', body: t(error) })
|
||||
redirectPage(router, 'drafts')
|
||||
}
|
||||
|
||||
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 [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 {
|
||||
setShout(loadedShout)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const title = createMemo(() => {
|
||||
if (!shout()) {
|
||||
return t('Create post')
|
||||
}
|
||||
|
||||
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 t(getContentTypeTitle(shout()?.layout as LayoutType))
|
||||
})
|
||||
|
||||
return (
|
||||
<PageLayout title={title()}>
|
||||
<AuthGuard>
|
||||
<Show when={shout()}>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<EditView shout={shout()} />
|
||||
</Suspense>
|
||||
</Show>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Show when={shout()} fallback={<Loading />}>
|
||||
<EditView shout={shout() as Shout} />
|
||||
</Show>
|
||||
</Suspense>
|
||||
</AuthGuard>
|
||||
</PageLayout>
|
||||
)
|
||||
|
|
|
@ -69,6 +69,7 @@ const checkOpenOnClient = (link: HTMLAnchorElement, event) => {
|
|||
)
|
||||
}
|
||||
|
||||
// TODO: use scrollToHash or remove
|
||||
const _scrollToHash = (hash: string) => {
|
||||
let selector = hash
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Author, QueryLoad_Authors_ByArgs } from '../../graphql/schema/core.gen'
|
|||
|
||||
export type AuthorsSortBy = 'shouts' | 'name' | 'followers'
|
||||
type SortedAuthorsSetter = (prev: Author[]) => Author[]
|
||||
|
||||
// FIXME: use signal or remove
|
||||
const [_sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('name')
|
||||
|
||||
export const setAuthorsSort = (sortBy: AuthorsSortBy) => setSortAllBy(sortBy)
|
||||
|
|
|
@ -2,6 +2,7 @@ import { RANDOM_TOPICS_COUNT } from '../components/Views/Home'
|
|||
import { Topic } from '../graphql/schema/core.gen'
|
||||
|
||||
export const getRandomTopicsFromArray = (topics: Topic[], count: number = RANDOM_TOPICS_COUNT): Topic[] => {
|
||||
if (!Array.isArray(topics)) return []
|
||||
const shuffledTopics = [...topics].sort(() => 0.5 - Math.random())
|
||||
return shuffledTopics.slice(0, count)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user