From 9e6ba5a523d8101185455deb6fb69aeb690c97ac Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 15 Dec 2023 16:45:34 +0300 Subject: [PATCH] session-connect-debug --- src/components/Article/FullArticle.tsx | 6 +- .../Nav/AuthModal/ForgotPasswordForm.tsx | 4 +- src/components/Nav/AuthModal/RegisterForm.tsx | 2 +- .../Nav/AuthModal/ResetPasswordForm.tsx | 0 src/components/Nav/HeaderAuth.tsx | 19 ++--- .../NotificationView/NotificationView.tsx | 2 +- .../ProfileSettings/ProfileSettings.tsx | 2 +- src/components/Topic/Card.tsx | 2 +- .../Topic/TopicBadge/TopicBadge.tsx | 2 +- src/components/Views/AllTopics.tsx | 4 +- src/components/Views/Edit.tsx | 2 +- src/components/Views/Feed.tsx | 2 +- src/components/Views/Inbox.tsx | 4 +- .../Views/PublishSettings/PublishSettings.tsx | 3 +- .../_shared/SolidSwiper/ImageSwiper.tsx | 2 +- src/context/connect.tsx | 83 +++++++++++-------- src/context/inbox.tsx | 15 ++-- src/context/notifications.tsx | 54 ++++++++---- src/context/session.tsx | 58 +++++++------ src/graphql/client/notifier.ts | 2 +- 20 files changed, 151 insertions(+), 117 deletions(-) delete mode 100644 src/components/Nav/AuthModal/ResetPasswordForm.tsx diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 5020b2e3..e32b73c0 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -75,7 +75,7 @@ export const FullArticle = (props: Props) => { const main_topic_slug = props.article.topics.length > 0 ? props.article.main_topic : null const mt = props.article.topics.find((tpc: Topic) => tpc.slug === main_topic_slug) if (mt) { - mt.title = lang() == 'en' ? capitalize(mt.slug.replace('-', ' ')) : mt.title + mt.title = lang() === 'en' ? capitalize(mt.slug.replace('-', ' ')) : mt.title return mt } else { return props.article.topics[0] @@ -329,7 +329,7 @@ export const FullArticle = (props: Props) => { >
{props.article.cover_caption} -
{props.article.cover_caption}
+
@@ -513,7 +513,7 @@ export const FullArticle = (props: Props) => { {(topic) => (
- {lang() == 'en' ? capitalize(topic.slug) : topic.title} + {lang() === 'en' ? capitalize(topic.slug) : topic.title}
)} diff --git a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx index ea7e48f0..aac65445 100644 --- a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx +++ b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx @@ -4,6 +4,7 @@ import { clsx } from 'clsx' import { createSignal, JSX, Show } from 'solid-js' import { useLocalize } from '../../../context/localize' +import { useSession } from '../../../context/session' import { ApiError } from '../../../graphql/error' import { useRouter } from '../../../stores/router' import { validateEmail } from '../../../utils/validateEmail' @@ -11,7 +12,6 @@ import { validateEmail } from '../../../utils/validateEmail' import { email, setEmail } from './sharedLogic' import styles from './AuthModal.module.scss' -import { useSession } from '../../../context/session' type FormFields = { email: string @@ -71,7 +71,7 @@ export const ForgotPasswordForm = () => { redirect_uri: window.location.href + '&success=1', // FIXME: redirect to success page accepting confirmation code }) if (response) { - console.debug(response) + console.debug('[ForgotPasswordForm]', response) if (response.message) setMessage(response.message) } } catch (error) { diff --git a/src/components/Nav/AuthModal/RegisterForm.tsx b/src/components/Nav/AuthModal/RegisterForm.tsx index b83c3190..93c59837 100644 --- a/src/components/Nav/AuthModal/RegisterForm.tsx +++ b/src/components/Nav/AuthModal/RegisterForm.tsx @@ -5,6 +5,7 @@ import { clsx } from 'clsx' import { Show, createSignal } from 'solid-js' import { useLocalize } from '../../../context/localize' +import { useSession } from '../../../context/session' import { ApiError } from '../../../graphql/error' import { checkEmail, useEmailChecks } from '../../../stores/emailChecks' import { useRouter } from '../../../stores/router' @@ -17,7 +18,6 @@ import { email, setEmail } from './sharedLogic' import { SocialProviders } from './SocialProviders' import styles from './AuthModal.module.scss' -import { useSession } from '../../../context/session' type FormFields = { fullName: string diff --git a/src/components/Nav/AuthModal/ResetPasswordForm.tsx b/src/components/Nav/AuthModal/ResetPasswordForm.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/Nav/HeaderAuth.tsx b/src/components/Nav/HeaderAuth.tsx index 5e25993d..bba8c902 100644 --- a/src/components/Nav/HeaderAuth.tsx +++ b/src/components/Nav/HeaderAuth.tsx @@ -134,28 +134,23 @@ export const HeaderAuth = (props: Props) => { - +
- +
- -
-
- - -
-
-
-
{renderIconedButton({ diff --git a/src/components/NotificationsPanel/NotificationView/NotificationView.tsx b/src/components/NotificationsPanel/NotificationView/NotificationView.tsx index 70b60919..f355344b 100644 --- a/src/components/NotificationsPanel/NotificationView/NotificationView.tsx +++ b/src/components/NotificationsPanel/NotificationView/NotificationView.tsx @@ -26,7 +26,7 @@ export const NotificationView = (props: Props) => { actions: { markNotificationAsRead, hideNotificationsPanel }, } = useNotifications() - const { changeSearchParam } = useRouter() + const { changeSearchParam } = useRouter() // TODO: use search params const { t, formatDate, formatTime } = useLocalize() diff --git a/src/components/ProfileSettings/ProfileSettings.tsx b/src/components/ProfileSettings/ProfileSettings.tsx index d5026dab..ed64ac65 100644 --- a/src/components/ProfileSettings/ProfileSettings.tsx +++ b/src/components/ProfileSettings/ProfileSettings.tsx @@ -4,10 +4,10 @@ import deepEqual from 'fast-deep-equal' import { createEffect, createSignal, For, lazy, Match, onCleanup, onMount, Show, Switch } from 'solid-js' import { createStore } from 'solid-js/store' -import { useSession } from '../../context/session' import { useConfirm } from '../../context/confirm' import { useLocalize } from '../../context/localize' import { useProfileForm } from '../../context/profile' +import { useSession } from '../../context/session' import { useSnackbar } from '../../context/snackbar' import { clone } from '../../utils/clone' import { getImageUrl } from '../../utils/getImageUrl' diff --git a/src/components/Topic/Card.tsx b/src/components/Topic/Card.tsx index 7cfc9973..23dd980e 100644 --- a/src/components/Topic/Card.tsx +++ b/src/components/Topic/Card.tsx @@ -84,7 +84,7 @@ export const TopicCard = (props: TopicProps) => { } const title = createMemo(() => - capitalize(lang() == 'en' ? props.topic.slug.replaceAll('-', ' ') : props.topic.title || ''), + capitalize(lang() === 'en' ? props.topic.slug.replaceAll('-', ' ') : props.topic.title || ''), ) return ( diff --git a/src/components/Topic/TopicBadge/TopicBadge.tsx b/src/components/Topic/TopicBadge/TopicBadge.tsx index 554d3f11..be5750f2 100644 --- a/src/components/Topic/TopicBadge/TopicBadge.tsx +++ b/src/components/Topic/TopicBadge/TopicBadge.tsx @@ -54,7 +54,7 @@ export const TopicBadge = (props: Props) => { /> - {lang() == 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title} + {lang() === 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title} { const byLetter = createMemo<{ [letter: string]: Topic[] }>(() => { return sortedTopics().reduce( (acc, topic) => { - let letter = lang() == 'en' ? topic.slug[0].toUpperCase() : topic.title[0].toUpperCase() + let letter = lang() === 'en' ? topic.slug[0].toUpperCase() : topic.title[0].toUpperCase() if (/[^ËА-яё]/.test(letter) && lang() === 'ru') letter = '#' if (/[^A-z]/.test(letter) && lang() === 'en') letter = '#' if (!acc[letter]) acc[letter] = [] @@ -147,7 +147,7 @@ export const AllTopicsView = (props: AllTopicsViewProps) => { {(topic) => (
- {lang() == 'en' + {lang() === 'en' ? capitalize(topic.slug.replaceAll('-', ' ')) : topic.title} diff --git a/src/components/Views/Edit.tsx b/src/components/Views/Edit.tsx index 6377a695..ca86b809 100644 --- a/src/components/Views/Edit.tsx +++ b/src/components/Views/Edit.tsx @@ -1,6 +1,6 @@ import { clsx } from 'clsx' import deepEqual from 'fast-deep-equal' -import { Accessor, createMemo, createSignal, lazy, onCleanup, onMount, Show, Suspense } from 'solid-js' +import { Accessor, createMemo, createSignal, lazy, onCleanup, onMount, Show } from 'solid-js' import { createStore } from 'solid-js/store' import { ShoutForm, useEditorContext } from '../../context/editor' diff --git a/src/components/Views/Feed.tsx b/src/components/Views/Feed.tsx index 231f6891..fa071dfb 100644 --- a/src/components/Views/Feed.tsx +++ b/src/components/Views/Feed.tsx @@ -228,7 +228,7 @@ export const FeedView = (props: Props) => { {(topic) => ( - {lang() == 'en' ? topic.slug.replaceAll('-', ' ') : topic.title} + {lang() === 'en' ? topic.slug.replaceAll('-', ' ') : topic.title} {' '} )} diff --git a/src/components/Views/Inbox.tsx b/src/components/Views/Inbox.tsx index ae4e1a01..20047839 100644 --- a/src/components/Views/Inbox.tsx +++ b/src/components/Views/Inbox.tsx @@ -53,7 +53,7 @@ export const InboxView = () => { const [isClear, setClear] = createSignal(false) const [isScrollToNewVisible, setIsScrollToNewVisible] = createSignal(false) const { author } = useSession() - const currentUserId = createMemo(() => author().id) + const currentUserId = createMemo(() => author()?.id) const { changeSearchParam, searchParams } = useRouter() const messagesContainerRef: { current: HTMLDivElement } = { @@ -100,7 +100,7 @@ export const InboxView = () => { const handleSubmit = async (message: string) => { await sendMessage({ body: message, - chat_id: currentDialog().id.toString(), + chat_id: currentDialog()?.id.toString(), reply_to: messageToReply()?.id, }) setClear(true) diff --git a/src/components/Views/PublishSettings/PublishSettings.tsx b/src/components/Views/PublishSettings/PublishSettings.tsx index 048a658c..e2477aec 100644 --- a/src/components/Views/PublishSettings/PublishSettings.tsx +++ b/src/components/Views/PublishSettings/PublishSettings.tsx @@ -1,12 +1,11 @@ import { redirectPage } from '@nanostores/router' import { clsx } from 'clsx' -import { createSignal, lazy, onMount, Show } from 'solid-js' +import { lazy, Show } from 'solid-js' import { createStore } from 'solid-js/store' import { ShoutForm, useEditorContext } from '../../../context/editor' import { useLocalize } from '../../../context/localize' import { useSession } from '../../../context/session' -import { Topic } from '../../../graphql/schema/core.gen' import { UploadedFile } from '../../../pages/types' import { router } from '../../../stores/router' import { hideModal, showModal } from '../../../stores/ui' diff --git a/src/components/_shared/SolidSwiper/ImageSwiper.tsx b/src/components/_shared/SolidSwiper/ImageSwiper.tsx index 888929c6..93864056 100644 --- a/src/components/_shared/SolidSwiper/ImageSwiper.tsx +++ b/src/components/_shared/SolidSwiper/ImageSwiper.tsx @@ -1,5 +1,5 @@ import { clsx } from 'clsx' -import { createEffect, createSignal, For, Show, on, onMount, lazy, onCleanup } from 'solid-js' +import { createEffect, createSignal, For, Show, on, onMount, onCleanup } from 'solid-js' import SwiperCore, { Manipulation, Navigation, Pagination } from 'swiper' import { throttle } from 'throttle-debounce' diff --git a/src/context/connect.tsx b/src/context/connect.tsx index 311032d4..c243cfd7 100644 --- a/src/context/connect.tsx +++ b/src/context/connect.tsx @@ -1,10 +1,12 @@ import type { Accessor, JSX } from 'solid-js' -import { fetchEventSource } from '@microsoft/fetch-event-source' +import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source' import { createContext, useContext, createSignal, createEffect } from 'solid-js' import { useSession } from './session' +const RECONNECT_TIMES = 2 + export interface SSEMessage { id: string entity: string @@ -36,42 +38,53 @@ export const ConnectProvider = (props: { children: JSX.Element }) => { const addHandler = (handler: MessageHandler) => { setHandlers((hhh) => [...hhh, handler]) } + const [retried, setRetried] = createSignal(0) - const listen = () => { - const token = getToken() - console.log(`[context.connect] token: ${token}`) - if (token && !connected() && retried() < 4) { - fetchEventSource('https://connect.discours.io', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: token, - }, - 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)) - }, - onclose() { - console.log('[context.connect] sse connection closed by server') - setConnected(false) - }, - onerror(err) { - console.error('[context.connect] sse connection error', err) - setRetried((r) => r + 1) - setConnected(false) - throw new Error(err) // NOTE: simple hack to close the connection - }, - }) - } - } - - createEffect(() => { + createEffect(async () => { if (isAuthenticated() && !connected()) { - listen() - setConnected(true) + const token = getToken() + if (token) { + await fetchEventSource('https://connect.discours.io', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: token, + }, + 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) { + console.log('[context.connect] SSE connection opened', response) + if (response.ok && response.headers.get('content-type') === EventStreamContentType) { + setConnected(true) + return + } else if (response.status === 401) { + throw new Error('unauthorized') + } else { + setRetried((r) => r + 1) + throw new Error() + } + }, + 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 + } else { + // do nothing to automatically retry. You can also + // return a specific retry interval here. + } + }, + }) + + return + } } }) diff --git a/src/context/inbox.tsx b/src/context/inbox.tsx index 4968a9e0..e2a74520 100644 --- a/src/context/inbox.tsx +++ b/src/context/inbox.tsx @@ -46,18 +46,23 @@ export const InboxProvider = (props: { children: JSX.Element }) => { } = useSession() const apiClient = createMemo(() => { const token = getToken() - if (!inboxClient.private) inboxClient.connect(token) - return inboxClient + if (!inboxClient.private) { + inboxClient.connect(token) + return inboxClient + } }) const { addHandler } = useConnect() addHandler(handleMessage) const loadChats = async () => { try { - const newChats = await apiClient().loadChats({ limit: 50, offset: 0 }) - setChats(newChats) + const client = apiClient() + if (client) { + const newChats = await client.loadChats({ limit: 50, offset: 0 }) + setChats(newChats) + } } catch (error) { - console.log('[loadChats]', error) + console.log('[loadChats] error: ', error) } } diff --git a/src/context/notifications.tsx b/src/context/notifications.tsx index cc1124a2..a41d77c1 100644 --- a/src/context/notifications.tsx +++ b/src/context/notifications.tsx @@ -1,6 +1,6 @@ import type { Accessor, JSX } from 'solid-js' -import { createContext, createMemo, createSignal, onMount, useContext } from 'solid-js' +import { createContext, createEffect, createMemo, createSignal, onMount, useContext } from 'solid-js' import { createStore } from 'solid-js/store' import { Portal } from 'solid-js/web' @@ -43,23 +43,35 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => { isAuthenticated, actions: { getToken }, } = useSession() + const apiClient = createMemo(() => { const token = getToken() - if (!notifierClient.private && isAuthenticated()) notifierClient.connect(token) - return notifierClient + if (!notifierClient.private) { + notifierClient.connect(token) + return notifierClient + } }) - const { addHandler } = useConnect() - const loadNotifications = async (options: { limit?: number; offset?: number }) => { - const { notifications, unread, total } = await apiClient().getNotifications(options) - const newNotificationEntities = notifications.reduce((acc, notification) => { - acc[notification.id] = notification - return acc - }, {}) - setTotalNotificationsCount(total) - setUnreadNotificationsCount(unread) - setNotificationEntities(newNotificationEntities) - return notifications + const { addHandler } = useConnect() + + const loadNotifications = async (options: { limit?: number; offset?: number }) => { + const client = apiClient() + if (isAuthenticated() && client) { + console.debug(client) + const { notifications, unread, total } = await client.getNotifications(options) + const newNotificationEntities = notifications.reduce((acc, notification) => { + acc[notification.id] = notification + return acc + }, {}) + + setTotalNotificationsCount(total) + setUnreadNotificationsCount(unread) + setNotificationEntities(newNotificationEntities) + console.debug(`[context.notifications] updated`) + return notifications + } else { + return [] + } } const sortedNotifications = createMemo(() => { @@ -70,7 +82,7 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => { onMount(() => { addHandler((data: SSEMessage) => { - if (data.entity === 'reaction') { + if (data.entity === 'reaction' && isAuthenticated()) { loadNotifications({ limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) }) } else { console.error(`[NotificationsProvider] unhandled message type: ${JSON.stringify(data)}`) @@ -79,14 +91,20 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => { }) const markNotificationAsRead = async (notification: Notification) => { - await apiClient().markNotificationAsRead(notification.id) + const client = apiClient() + if (client) { + await client.markNotificationAsRead(notification.id) + } const nnn = new Set([...notification.seen, notification.id]) setNotificationEntities(notification.id, 'seen', [...nnn]) setUnreadNotificationsCount((oldCount) => oldCount - 1) } const markAllNotificationsAsRead = async () => { - await apiClient().markAllNotificationsAsRead() - loadNotifications({ limit: loadedNotificationsCount() }) + const client = apiClient() + if (isAuthenticated() && client) { + await client.markAllNotificationsAsRead() + await loadNotifications({ limit: loadedNotificationsCount() }) + } } const showNotificationsPanel = () => { diff --git a/src/context/session.tsx b/src/context/session.tsx index bfa116eb..f2c4aa06 100644 --- a/src/context/session.tsx +++ b/src/context/session.tsx @@ -23,6 +23,7 @@ import { import { apiClient } from '../graphql/client/core' import { showModal } from '../stores/ui' + import { useLocalize } from './localize' import { useSnackbar } from './snackbar' @@ -74,14 +75,16 @@ export const SessionProvider = (props: { onStateChangeCallback(state: any): unknown children: JSX.Element }) => { - const [isSessionLoaded, setIsSessionLoaded] = createSignal(false) - const [subscriptions, setSubscriptions] = createSignal(EMPTY_SUBSCRIPTIONS) const { t } = useLocalize() const { actions: { showSnackbar }, } = useSnackbar() + + const [isSessionLoaded, setIsSessionLoaded] = createSignal(false) + const [subscriptions, setSubscriptions] = createSignal(EMPTY_SUBSCRIPTIONS) const [token, setToken] = createSignal() const [user, setUser] = createSignal() + const loadSubscriptions = async (): Promise => { const result = await apiClient.getMySubscriptions() if (result) { @@ -91,24 +94,30 @@ export const SessionProvider = (props: { } } + const setAuth = (auth: AuthToken | void) => { + if (auth) { + setToken(auth) + setUser(auth.user) + mutate(auth) + } + } + const getSession = async (): Promise => { try { - const t = getToken() - console.debug(t) + const tkn = getToken() + console.debug('[context.session] token before: ', tkn) const authResult = await authorizer().getSession({ - Authorization: t, + Authorization: tkn, }) if (authResult?.access_token) { - console.log(authResult) - setToken(authResult) - if (authResult.user) setUser(authResult.user) - loadSubscriptions() + setAuth(authResult) + console.debug('[context.session] token after: ', authResult.access_token) + await loadSubscriptions() return authResult } } catch (error) { - console.error('getSession error:', error) - setToken(null) - setUser(null) + console.error('[context.session] getSession error:', error) + setAuth(null) return null } finally { setTimeout(() => { @@ -142,10 +151,9 @@ export const SessionProvider = (props: { const authResult: AuthToken | void = await authorizer().login(params) if (authResult && authResult.access_token) { - setToken(authResult) - mutate(authResult) - loadSubscriptions() - console.debug('signed in') + setAuth(authResult) + await loadSubscriptions() + console.debug('[context.session] signed in') } else { console.info((authResult as AuthToken).message) } @@ -179,10 +187,10 @@ export const SessionProvider = (props: { setConfig({ ...config, ...metaRes, redirectURL: window.location.origin + '/?modal=auth' }) console.log('[context.session] refreshing session...') const s = await getSession() - console.debug(s) - setToken(s) + console.debug('[context.session] session: ', s) console.log('[context.session] loading author...') - await loadAuthor() + const a = await loadAuthor() + console.debug('[context.session] author: ', a) setIsSessionLoaded(true) console.log('[context.session] loaded') }) @@ -191,7 +199,8 @@ export const SessionProvider = (props: { const requireAuthentication = async (callback: () => void, modalSource: AuthModalSource) => { setIsAuthWithCallback(() => callback) - await authorizer().getProfile() + const userdata = await authorizer().getProfile() + if (userdata) setUser(userdata) if (!isAuthenticated()) { showModal('auth', modalSource) @@ -200,19 +209,14 @@ export const SessionProvider = (props: { const signOut = async () => { await authorizer().logout() - mutate(null) - setToken(null) - setUser(null) + setAuth(null) setSubscriptions(EMPTY_SUBSCRIPTIONS) showSnackbar({ body: t("You've successfully logged out") }) } const confirmEmail = async (input: VerifyEmailInput) => { const at: void | AuthToken = await authorizer().verifyEmail(input) - if (at) { - setToken(at) - mutate(at) - } + setAuth(at) } const getToken = createMemo(() => token()?.access_token) diff --git a/src/graphql/client/notifier.ts b/src/graphql/client/notifier.ts index eb5b3396..676ff0a9 100644 --- a/src/graphql/client/notifier.ts +++ b/src/graphql/client/notifier.ts @@ -10,7 +10,7 @@ export const notifierClient = { getNotifications: async (params: QueryLoad_NotificationsArgs): Promise => { const resp = await notifierClient.private.query(loadNotifications, params).toPromise() - return resp.data.load_notifications + return resp.data?.load_notifications }, markNotificationAsRead: async (notification_id: number): Promise => { await notifierClient.private