From 317d4a004cd1aa984def3b50c62ea649a82e757d Mon Sep 17 00:00:00 2001 From: Untone Date: Tue, 24 Sep 2024 12:15:50 +0300 Subject: [PATCH] graphql-client-unmemo --- src/components/Article/Comment/Comment.tsx | 5 +- src/components/Author/AuthorRatingControl.tsx | 7 +- src/components/Views/Author/Author.tsx | 5 +- .../Views/DraftsView/DraftsView.tsx | 48 ++-- .../Views/EditView/EditSettingsView.tsx | 7 +- src/components/Views/EditView/EditView.tsx | 8 +- src/components/Views/Expo/Expo.tsx | 5 +- src/components/Views/Feed/Feed.tsx | 7 +- src/config.ts | 14 +- src/context/editor.tsx | 7 +- src/context/feed.tsx | 7 +- src/context/following.tsx | 21 +- src/context/inbox.tsx | 7 +- src/context/notifications.tsx | 15 +- src/context/profile.tsx | 18 +- src/context/reactions.tsx | 7 +- src/context/session.tsx | 205 +++++++++++++----- src/intl/locales/ru/translation.json | 1 + src/routes/edit/(drafts).tsx | 8 +- src/routes/edit/[id]/(draft).tsx | 47 ++-- src/routes/edit/[id]/settings.tsx | 36 +-- src/routes/edit/new.tsx | 10 +- src/routes/feed/my/[...mode]/[...order].tsx | 6 +- 23 files changed, 274 insertions(+), 227 deletions(-) diff --git a/src/components/Article/Comment/Comment.tsx b/src/components/Article/Comment/Comment.tsx index 039701d3..6f0125c2 100644 --- a/src/components/Article/Comment/Comment.tsx +++ b/src/components/Article/Comment/Comment.tsx @@ -3,12 +3,10 @@ import { clsx } from 'clsx' import { For, Show, Suspense, createMemo, createSignal, lazy } from 'solid-js' import { Icon } from '~/components/_shared/Icon' import { ShowIfAuthenticated } from '~/components/_shared/ShowIfAuthenticated' -import { coreApiUrl } from '~/config' import { useLocalize } from '~/context/localize' import { useReactions } from '~/context/reactions' import { useSession } from '~/context/session' import { useSnackbar, useUI } from '~/context/ui' -import { graphqlClientCreate } from '~/graphql/client' import deleteReactionMutation from '~/graphql/mutation/core/reaction-destroy' import { Author, @@ -45,12 +43,11 @@ export const Comment = (props: Props) => { const [editMode, setEditMode] = createSignal(false) const [clearEditor, setClearEditor] = createSignal(false) const [editedBody, setEditedBody] = createSignal() - const { session } = useSession() + const { session, client } = useSession() const author = createMemo(() => session()?.user?.app_data?.profile as Author) const { createShoutReaction, updateShoutReaction } = useReactions() const { showConfirm } = useUI() const { showSnackbar } = useSnackbar() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) const canEdit = createMemo( () => Boolean(author()?.id) && diff --git a/src/components/Author/AuthorRatingControl.tsx b/src/components/Author/AuthorRatingControl.tsx index 531ec61e..a002de70 100644 --- a/src/components/Author/AuthorRatingControl.tsx +++ b/src/components/Author/AuthorRatingControl.tsx @@ -1,10 +1,8 @@ import type { Author } from '~/graphql/schema/core.gen' import { clsx } from 'clsx' -import { Show, createMemo, createSignal } from 'solid-js' -import { coreApiUrl } from '~/config' +import { Show, createSignal } from 'solid-js' import { useSession } from '~/context/session' -import { graphqlClientCreate } from '~/graphql/client' import rateAuthorMutation from '~/graphql/mutation/core/author-rate' import styles from './AuthorRatingControl.module.scss' @@ -17,8 +15,7 @@ export const AuthorRatingControl = (props: AuthorRatingControlProps) => { const isUpvoted = false const isDownvoted = false - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { client } = useSession() // eslint-disable-next-line unicorn/consistent-function-scoping const handleRatingChange = async (isUpvote: boolean) => { diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index 1deaa13b..7e1003fc 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -3,7 +3,6 @@ import { clsx } from 'clsx' import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on } from 'solid-js' import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper' import { Loading } from '~/components/_shared/Loading' -import { coreApiUrl } from '~/config' import { useAuthors } from '~/context/authors' import { SHOUTS_PER_PAGE, useFeed } from '~/context/feed' import { useFollowing } from '~/context/following' @@ -11,7 +10,6 @@ import { useLocalize } from '~/context/localize' import { useReactions } from '~/context/reactions' import { useSession } from '~/context/session' import { loadReactions, loadShouts } from '~/graphql/api/public' -import { graphqlClientCreate } from '~/graphql/client' import getAuthorFollowersQuery from '~/graphql/query/core/author-followers' import getAuthorFollowsQuery from '~/graphql/query/core/author-follows' import type { Author, Reaction, Shout, Topic } from '~/graphql/schema/core.gen' @@ -45,8 +43,7 @@ export const AuthorView = (props: AuthorViewProps) => { const params = useParams() const [currentTab, setCurrentTab] = createSignal(params.tab) - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { session, client } = useSession() const { loadAuthor, authorsEntities } = useAuthors() const { followers: myFollowers, follows: myFollows } = useFollowing() diff --git a/src/components/Views/DraftsView/DraftsView.tsx b/src/components/Views/DraftsView/DraftsView.tsx index cdaae1e1..c2de14a2 100644 --- a/src/components/Views/DraftsView/DraftsView.tsx +++ b/src/components/Views/DraftsView/DraftsView.tsx @@ -1,17 +1,14 @@ import { useNavigate } from '@solidjs/router' import { clsx } from 'clsx' -import { For, Show, createMemo, createSignal } from 'solid-js' +import { For, Show, createSignal } from 'solid-js' import { Draft } from '~/components/Draft' -import { Loading } from '~/components/_shared/Loading' import { useEditorContext } from '~/context/editor' -import { useSession } from '~/context/session' +import { useLocalize } from '~/context/localize' import { Shout } from '~/graphql/schema/core.gen' import styles from './DraftsView.module.scss' export const DraftsView = (props: { drafts: Shout[] }) => { const [drafts, setDrafts] = createSignal(props.drafts || []) - const { session } = useSession() - const authorized = createMemo(() => Boolean(session()?.access_token)) const navigate = useNavigate() const { publishShoutById, deleteShout } = useEditorContext() const handleDraftDelete = async (shout: Shout) => { @@ -26,26 +23,33 @@ export const DraftsView = (props: { drafts: Shout[] }) => { setTimeout(() => navigate('/feed'), 2000) } + const { t } = useLocalize() + return (
- }> -
-
-
- - {(draft) => ( - - )} - -
-
+
+
+

{t('Drafts')}

- + + {(ddd) => ( +
+
+ + {(draft) => ( + + )} + +
+
+ )} +
+
) } diff --git a/src/components/Views/EditView/EditSettingsView.tsx b/src/components/Views/EditView/EditSettingsView.tsx index a410f9a4..351c42c7 100644 --- a/src/components/Views/EditView/EditSettingsView.tsx +++ b/src/components/Views/EditView/EditSettingsView.tsx @@ -1,15 +1,13 @@ import { clsx } from 'clsx' import deepEqual from 'fast-deep-equal' -import { Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js' +import { Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js' import { createStore } from 'solid-js/store' import { debounce } from 'throttle-debounce' import { Icon } from '~/components/_shared/Icon' import { InviteMembers } from '~/components/_shared/InviteMembers' -import { coreApiUrl } from '~/config' import { ShoutForm, useEditorContext } from '~/context/editor' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' -import { graphqlClientCreate } from '~/graphql/client' import getMyShoutQuery from '~/graphql/query/core/article-my' import type { Shout, Topic } from '~/graphql/schema/core.gen' import { isDesktop } from '~/lib/mediaQuery' @@ -44,8 +42,7 @@ const handleScrollTopButtonClick = (ev: MouseEvent | TouchEvent) => { export const EditSettingsView = (props: Props) => { const { t } = useLocalize() const [isScrolled, setIsScrolled] = createSignal(false) - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { client } = useSession() const { form, setForm, saveDraft, saveDraftToLocalStorage, getDraftFromLocalStorage } = useEditorContext() const [shoutTopics, setShoutTopics] = createSignal([]) const [draft, setDraft] = createSignal() diff --git a/src/components/Views/EditView/EditView.tsx b/src/components/Views/EditView/EditView.tsx index 26d78db7..0e22f777 100644 --- a/src/components/Views/EditView/EditView.tsx +++ b/src/components/Views/EditView/EditView.tsx @@ -1,6 +1,6 @@ import { clsx } from 'clsx' import deepEqual from 'fast-deep-equal' -import { Show, createEffect, createMemo, createSignal, lazy, on, onCleanup, onMount } from 'solid-js' +import { Show, createEffect, createSignal, lazy, on, onCleanup, onMount } from 'solid-js' import { createStore } from 'solid-js/store' import { debounce } from 'throttle-debounce' import { DropArea } from '~/components/_shared/DropArea' @@ -9,11 +9,9 @@ import { InviteMembers } from '~/components/_shared/InviteMembers' import { Loading } from '~/components/_shared/Loading' import { Popover } from '~/components/_shared/Popover' import { EditorSwiper } from '~/components/_shared/SolidSwiper' -import { coreApiUrl } from '~/config' import { ShoutForm, useEditorContext } from '~/context/editor' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' -import { graphqlClientCreate } from '~/graphql/client' import getMyShoutQuery from '~/graphql/query/core/article-my' import type { Shout, Topic } from '~/graphql/schema/core.gen' import { slugify } from '~/intl/translit' @@ -55,7 +53,7 @@ const handleScrollTopButtonClick = (ev: MouseEvent | TouchEvent) => { export const EditView = (props: Props) => { const { t } = useLocalize() - const { session } = useSession() + const { client } = useSession() const { form, formErrors, @@ -76,8 +74,6 @@ export const EditView = (props: Props) => { const [draft, setDraft] = createSignal(props.shout) const [mediaItems, setMediaItems] = createSignal([]) - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) - createEffect(() => setMediaItems(JSON.parse(form.media || '[]'))) createEffect( diff --git a/src/components/Views/Expo/Expo.tsx b/src/components/Views/Expo/Expo.tsx index 6ecc017b..294a9748 100644 --- a/src/components/Views/Expo/Expo.tsx +++ b/src/components/Views/Expo/Expo.tsx @@ -5,12 +5,10 @@ import { ConditionalWrapper } from '~/components/_shared/ConditionalWrapper' import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper' import { Loading } from '~/components/_shared/Loading' import { ArticleCardSwiper } from '~/components/_shared/SolidSwiper/ArticleCardSwiper' -import { coreApiUrl } from '~/config' import { EXPO_LAYOUTS, SHOUTS_PER_PAGE, useFeed } from '~/context/feed' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' import { loadShouts } from '~/graphql/api/public' -import { graphqlClientCreate } from '~/graphql/client' import getRandomTopShoutsQuery from '~/graphql/query/core/articles-load-random-top' import { LoadShoutsFilters, LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' import { LayoutType } from '~/types/common' @@ -31,8 +29,7 @@ const LOAD_MORE_PAGE_SIZE = 12 export const Expo = (props: Props) => { const { t } = useLocalize() - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { client } = useSession() const [favoriteTopArticles, setFavoriteTopArticles] = createSignal([]) const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal([]) diff --git a/src/components/Views/Feed/Feed.tsx b/src/components/Views/Feed/Feed.tsx index b083104d..3fc889db 100644 --- a/src/components/Views/Feed/Feed.tsx +++ b/src/components/Views/Feed/Feed.tsx @@ -7,7 +7,6 @@ import { Icon } from '~/components/_shared/Icon' import { InviteMembers } from '~/components/_shared/InviteMembers' import { Loading } from '~/components/_shared/Loading' import { ShareModal } from '~/components/_shared/ShareModal' -import { coreApiUrl } from '~/config' import { useAuthors } from '~/context/authors' import { useLocalize } from '~/context/localize' import { useReactions } from '~/context/reactions' @@ -15,7 +14,6 @@ import { useSession } from '~/context/session' import { useTopics } from '~/context/topics' import { useUI } from '~/context/ui' import { loadUnratedShouts } from '~/graphql/api/private' -import { graphqlClientCreate } from '~/graphql/client' import type { Author, Reaction, Shout } from '~/graphql/schema/core.gen' import { FeedSearchParams } from '~/routes/feed/[...order]' import { byCreated } from '~/utils/sort' @@ -49,11 +47,10 @@ const PERIODS = { export const FeedView = (props: FeedProps) => { const { t } = useLocalize() const loc = useLocation() - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { client, session } = useSession() const unrated = createAsync(async () => { - if (client) { + if (client()) { const shoutsLoader = loadUnratedShouts(client(), { limit: 5 }) return await shoutsLoader() } diff --git a/src/config.ts b/src/config.ts index 7842ada7..f34c0972 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,9 +1,17 @@ -export const isDev = import.meta.env.MODE === 'development' export const cdnUrl = 'https://cdn.discours.io' export const thumborUrl = import.meta.env.PUBLIC_THUMBOR_URL || 'https://images.discours.io' -export const reportDsn = import.meta.env.PUBLIC_GLITCHTIP_DSN || import.meta.env.PUBLIC_SENTRY_DSN || '' export const coreApiUrl = import.meta.env.PUBLIC_CORE_API || 'https://core.discours.io' export const chatApiUrl = import.meta.env.PUBLIC_CHAT_API || 'https://inbox.discours.io' export const authApiUrl = import.meta.env.PUBLIC_AUTH_API || 'https://auth.discours.io/graphql' export const sseUrl = import.meta.env.PUBLIC_REALTIME_EVENTS || 'https://connect.discours.io' -export const gaIdentity = import.meta.env.PUBLIC_GA_IDENTITY || '' // 'G-LQ4B87H8C2' +export const gaIdentity = import.meta.env.PUBLIC_GA_IDENTITY || 'G-LQ4B87H8C2' +export const authorizerClientId = + import.meta.env.PUBLIC_AUTHORIZER_CLIENT_ID || 'b9038a34-ca59-41ae-a105-c7fbea603e24' +export const authorizerRedirectUrl = + import.meta.env.PUBLIC_AUTHORIZER_REDIRECT_URL || 'https://testing.discours.io' + +// devmode only +export const isDev = import.meta.env.MODE === 'development' +export const reportDsn = isDev + ? import.meta.env.PUBLIC_GLITCHTIP_DSN || import.meta.env.PUBLIC_SENTRY_DSN || '' + : '' diff --git a/src/context/editor.tsx b/src/context/editor.tsx index 83ef43e7..3e3f569b 100644 --- a/src/context/editor.tsx +++ b/src/context/editor.tsx @@ -1,17 +1,15 @@ import { useMatch, useNavigate } from '@solidjs/router' import { Editor, EditorOptions } from '@tiptap/core' import type { JSX } from 'solid-js' -import { Accessor, createContext, createMemo, createSignal, useContext } from 'solid-js' +import { Accessor, createContext, createSignal, useContext } from 'solid-js' import { SetStoreFunction, createStore } from 'solid-js/store' import { createTiptapEditor } from 'solid-tiptap' -import { coreApiUrl } from '~/config' import { useSnackbar } from '~/context/ui' import deleteShoutQuery from '~/graphql/mutation/core/article-delete' import updateShoutQuery from '~/graphql/mutation/core/article-update' import { Topic, TopicInput } from '~/graphql/schema/core.gen' import { slugify } from '~/intl/translit' import { useFeed } from '../context/feed' -import { graphqlClientCreate } from '../graphql/client' import { useLocalize } from './localize' import { useSession } from './session' @@ -85,8 +83,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => { const navigate = useNavigate() const matchEdit = useMatch(() => '/edit') const matchEditSettings = useMatch(() => '/editSettings') - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { client } = useSession() const [editor, setEditor] = createSignal() const { addFeed } = useFeed() const snackbar = useSnackbar() diff --git a/src/context/feed.tsx b/src/context/feed.tsx index 3380c1ac..7ce2c0c2 100644 --- a/src/context/feed.tsx +++ b/src/context/feed.tsx @@ -1,7 +1,6 @@ import { createLazyMemo } from '@solid-primitives/memo' import { makePersisted } from '@solid-primitives/storage' -import { Accessor, JSX, Setter, createContext, createMemo, createSignal, useContext } from 'solid-js' -import { coreApiUrl } from '~/config' +import { Accessor, JSX, Setter, createContext, createSignal, useContext } from 'solid-js' import { loadFollowedShouts } from '~/graphql/api/private' import { loadShoutsSearch as fetchShoutsSearch, getShout, loadShouts } from '~/graphql/api/public' import { @@ -12,7 +11,6 @@ import { Topic } from '~/graphql/schema/core.gen' import { LayoutType } from '~/types/common' -import { graphqlClientCreate } from '../graphql/client' import { byStat } from '../utils/sort' import { useSession } from './session' @@ -176,8 +174,7 @@ export const FeedProvider = (props: { children: JSX.Element }) => { addFeed(result) return { hasMore, newShouts: result } } - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { client } = useSession() // Load the user's feed based on the provided options and update the articleEntities and sortedFeed state const loadMyFeed = async ( diff --git a/src/context/following.tsx b/src/context/following.tsx index 4ae3e25c..98432dd3 100644 --- a/src/context/following.tsx +++ b/src/context/following.tsx @@ -1,21 +1,10 @@ -import { - Accessor, - JSX, - createContext, - createEffect, - createMemo, - createSignal, - on, - useContext -} from 'solid-js' +import { Accessor, JSX, createContext, createEffect, createSignal, on, useContext } from 'solid-js' import { createStore } from 'solid-js/store' -import { coreApiUrl } from '~/config' import followMutation from '~/graphql/mutation/core/follow' import unfollowMutation from '~/graphql/mutation/core/unfollow' import loadAuthorFollowers from '~/graphql/query/core/author-followers' import { Author, Community, FollowingEntity, Topic } from '~/graphql/schema/core.gen' -import { graphqlClientCreate } from '../graphql/client' import { useSession } from './session' export type FollowsFilter = 'all' | 'authors' | 'topics' | 'communities' @@ -70,9 +59,7 @@ export const FollowingProvider = (props: { children: JSX.Element }) => { const [loading, setLoading] = createSignal(false) const [followers, setFollowers] = createSignal([] as Author[]) const [follows, setFollows] = createStore(EMPTY_SUBSCRIPTIONS) - const { session } = useSession() - const authorized = createMemo(() => Boolean(session()?.access_token)) - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { session, client } = useSession() const fetchData = async () => { setLoading(true) @@ -96,7 +83,7 @@ export const FollowingProvider = (props: { children: JSX.Element }) => { const [following, setFollowing] = createSignal(defaultFollowing) const follow = async (what: FollowingEntity, slug: string) => { - if (!authorized()) return + if (!session()?.access_token) return setFollowing({ slug, type: 'follow' }) try { const resp = await client()?.mutation(followMutation, { what, slug }).toPromise() @@ -115,7 +102,7 @@ export const FollowingProvider = (props: { children: JSX.Element }) => { } const unfollow = async (what: FollowingEntity, slug: string) => { - if (!authorized()) return + if (!session()?.access_token) return setFollowing({ slug: slug, type: 'unfollow' }) try { const resp = await client()?.mutation(unfollowMutation, { what, slug }).toPromise() diff --git a/src/context/inbox.tsx b/src/context/inbox.tsx index c95b4719..261ae0e6 100644 --- a/src/context/inbox.tsx +++ b/src/context/inbox.tsx @@ -1,7 +1,5 @@ import type { Accessor, JSX } from 'solid-js' -import { createContext, createMemo, createSignal, useContext } from 'solid-js' -import { chatApiUrl } from '~/config' -import { graphqlClientCreate } from '~/graphql/client' +import { createContext, createSignal, useContext } from 'solid-js' import createChatMutation from '~/graphql/mutation/chat/chat-create' import createMessageMutation from '~/graphql/mutation/chat/chat-message-create' import loadChatMessagesQuery from '~/graphql/query/chat/chat-messages-load-by' @@ -38,8 +36,7 @@ export const InboxProvider = (props: { children: JSX.Element }) => { const [chats, setChats] = createSignal([]) const [messages, setMessages] = createSignal([]) const { authorsSorted } = useAuthors() - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(chatApiUrl, session()?.access_token)) + const { client } = useSession() const handleMessage = (sseMessage: SSEMessage) => { // handling all action types: create update delete join left seen diff --git a/src/context/notifications.tsx b/src/context/notifications.tsx index 591898f3..ddf6e333 100644 --- a/src/context/notifications.tsx +++ b/src/context/notifications.tsx @@ -1,12 +1,9 @@ import { makePersisted } from '@solid-primitives/storage' import type { Accessor, JSX } from 'solid-js' - import { createContext, createMemo, createSignal, onMount, useContext } from 'solid-js' import { createStore } from 'solid-js/store' import { Portal } from 'solid-js/web' -import { coreApiUrl } from '~/config' -import { graphqlClientCreate } from '~/graphql/client' import markSeenMutation from '~/graphql/mutation/notifier/mark-seen' import markSeenAfterMutation from '~/graphql/mutation/notifier/mark-seen-after' import markSeenThreadMutation from '~/graphql/mutation/notifier/mark-seen-thread' @@ -47,13 +44,11 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => { const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0) const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0) const [notificationEntities, setNotificationEntities] = createStore>({}) - const { session } = useSession() - const authorized = createMemo(() => Boolean(session()?.access_token)) + const { session, client } = useSession() const { addHandler } = useConnect() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) const loadNotificationsGrouped = async (options: QueryLoad_NotificationsArgs) => { - if (authorized()) { + if (session()?.access_token) { const resp = await client()?.query(getNotifications, options).toPromise() const result = resp?.data?.get_notifications const groups = result?.notifications || [] @@ -87,7 +82,7 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => { onMount(() => { addHandler((data: SSEMessage) => { - if (data.entity === 'reaction' && authorized()) { + if (data.entity === 'reaction' && session()?.access_token) { console.info('[context.notifications] event', data) loadNotificationsGrouped({ after: after() || now, @@ -107,14 +102,14 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => { } const markSeenAll = async () => { - if (authorized()) { + if (session()?.access_token) { const _resp = await client()?.mutation(markSeenAfterMutation, { after: after() }).toPromise() await loadNotificationsGrouped({ after: after() || now, limit: loadedNotificationsCount() }) } } const markSeen = async (notification_id: number) => { - if (authorized()) { + if (session()?.access_token) { await client()?.mutation(markSeenMutation, { notification_id }).toPromise() await loadNotificationsGrouped({ after: after() || now, limit: loadedNotificationsCount() }) } diff --git a/src/context/profile.tsx b/src/context/profile.tsx index 7446732a..0a177029 100644 --- a/src/context/profile.tsx +++ b/src/context/profile.tsx @@ -1,20 +1,9 @@ import type { Author, ProfileInput } from '~/graphql/schema/core.gen' import { AuthToken } from '@authorizerdev/authorizer-js' -import { - Accessor, - JSX, - createContext, - createEffect, - createMemo, - createSignal, - on, - useContext -} from 'solid-js' +import { Accessor, JSX, createContext, createEffect, createSignal, on, useContext } from 'solid-js' import { createStore } from 'solid-js/store' -import { coreApiUrl } from '~/config' import updateAuthorMuatation from '~/graphql/mutation/core/author-update' -import { graphqlClientCreate } from '../graphql/client' import { useAuthors } from './authors' import { useSession } from './session' @@ -41,8 +30,7 @@ const userpicUrl = (userpic: string) => { } export const ProfileProvider = (props: { children: JSX.Element }) => { - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { session, client } = useSession() const { addAuthor } = useAuthors() const [form, setForm] = createStore({} as ProfileInput) const [author, setAuthor] = createSignal({} as Author) @@ -66,7 +54,7 @@ export const ProfileProvider = (props: { children: JSX.Element }) => { const submit = async (profile: ProfileInput) => { const response = await client()?.mutation(updateAuthorMuatation, profile).toPromise() - if (response.error) { + if (response?.error) { console.error(response.error) throw response.error } diff --git a/src/context/reactions.tsx b/src/context/reactions.tsx index 911d7352..583da997 100644 --- a/src/context/reactions.tsx +++ b/src/context/reactions.tsx @@ -1,6 +1,5 @@ import type { Accessor, JSX } from 'solid-js' -import { createContext, createMemo, createSignal, onCleanup, useContext } from 'solid-js' -import { coreApiUrl } from '~/config' +import { createContext, createSignal, onCleanup, useContext } from 'solid-js' import { loadReactions } from '~/graphql/api/public' import createReactionMutation from '~/graphql/mutation/core/reaction-create' import destroyReactionMutation from '~/graphql/mutation/core/reaction-destroy' @@ -12,7 +11,6 @@ import { Reaction, ReactionKind } from '~/graphql/schema/core.gen' -import { graphqlClientCreate } from '../graphql/client' import { useLocalize } from './localize' import { useSession } from './session' import { useSnackbar } from './ui' @@ -41,8 +39,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { const [commentsByAuthor, setCommentsByAuthor] = createSignal>({}) const { t } = useLocalize() const { showSnackbar } = useSnackbar() - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { client } = useSession() const addShoutReactions = (rrr: Reaction[]) => { const newReactionEntities = { ...reactionEntities() } diff --git a/src/context/session.tsx b/src/context/session.tsx index 12a3f072..a4f58ba3 100644 --- a/src/context/session.tsx +++ b/src/context/session.tsx @@ -12,6 +12,7 @@ import { VerifyEmailInput } from '@authorizerdev/authorizer-js' import { useSearchParams } from '@solidjs/router' +import { Client } from '@urql/core' import type { Accessor, JSX, Resource } from 'solid-js' import { createContext, @@ -25,13 +26,14 @@ import { useContext } from 'solid-js' import { type AuthModalSource, useSnackbar, useUI } from '~/context/ui' -import { authApiUrl } from '../config' +import { graphqlClientCreate } from '~/graphql/client' +import { authApiUrl, authorizerClientId, authorizerRedirectUrl, coreApiUrl } from '../config' import { useLocalize } from './localize' const defaultConfig: ConfigType = { authorizerURL: authApiUrl.replace('/graphql', ''), - redirectURL: 'https://testing.discours.io', - clientID: 'b9038a34-ca59-41ae-a105-c7fbea603e24' + redirectURL: authorizerRedirectUrl, + clientID: authorizerClientId } export type SessionContextType = { @@ -51,20 +53,22 @@ export type SessionContextType = { signOut: () => Promise oauth: (provider: string) => Promise forgotPassword: (params: ForgotPasswordInput) => Promise - changePassword: (password: string, token: string) => void + changePassword: (password: string, token: string) => Promise confirmEmail: (input: VerifyEmailInput) => Promise setIsSessionLoaded: (loaded: boolean) => void authorizer: () => Authorizer isRegistered: (email: string) => Promise resendVerifyEmail: (params: ResendVerifyEmailInput) => Promise + client: Accessor } const noop = () => null + const metaRes = { data: { meta: { version: 'latest', - client_id: 'b9038a34-ca59-41ae-a105-c7fbea603e24', + client_id: authorizerClientId, is_google_login_enabled: true, is_facebook_login_enabled: true, is_github_login_enabled: true, @@ -86,12 +90,21 @@ const metaRes = { } } +/** + * Session context to manage authentication state and provide authentication functions. + */ export const SessionContext = createContext({} as SessionContextType) export function useSession() { return useContext(SessionContext) } +/** + * SessionProvider component that wraps its children with session context. + * It handles session management, authentication, and provides related functions. + * @param props - The props containing an onStateChangeCallback function and children elements. + * @returns A JSX Element wrapping the children with session context. + */ export const SessionProvider = (props: { onStateChangeCallback(state: AuthToken): unknown children: JSX.Element @@ -113,45 +126,55 @@ export const SessionProvider = (props: { const authorizer = createMemo(() => new Authorizer(config())) const [oauthState, setOauthState] = createSignal() - // load - let minuteLater: NodeJS.Timeout | null + // Session expiration timer + let minuteLater: ReturnType | null = null const [isSessionLoaded, setIsSessionLoaded] = createSignal(false) const [authError, setAuthError] = createSignal('') const { showModal } = useUI() - // handle auth state callback from outside + // Handle auth state callback from outside onMount(() => { const params = searchParams if (params?.state) { - setOauthState((_s) => params?.state) - const scope = params?.scope ? params?.scope?.toString().split(' ') : ['openid', 'profile', 'email'] + setOauthState(params.state) + const scope = params.scope ? params.scope.toString().split(' ') : ['openid', 'profile', 'email'] if (scope) console.info(`[context.session] scope: ${scope}`) - const url = params?.redirect_uri || params?.redirectURL || window.location.href + const url = params.redirect_uri || params.redirectURL || window.location.href setConfig((c: ConfigType) => ({ ...c, redirectURL: url.split('?')[0] })) changeSearchParams({ mode: 'confirm-email', m: 'auth' }, { replace: true }) } }) - // handle token confirm + // Handle token confirmation createEffect(() => { const token = searchParams?.token const access_token = searchParams?.access_token - if (access_token) - changeSearchParams({ - mode: 'confirm-email', - m: 'auth', - access_token - }) - else if (token) { - changeSearchParams({ - mode: 'change-password', - m: 'auth', - token - }) + if (access_token) { + changeSearchParams( + { + mode: 'confirm-email', + m: 'auth', + access_token + }, + { replace: true } + ) + } else if (token) { + changeSearchParams( + { + mode: 'change-password', + m: 'auth', + token + }, + { replace: true } + ) } }) - // Function to load session data + /** + * Function to load session data by fetching the current session from the authorizer. + * It handles session expiration and sets up a timer to refresh the session as needed. + * @returns A Promise resolving to the AuthToken containing session information. + */ const sessionData = async () => { try { const s: ApiResponse = await authorizer().getSession() @@ -191,6 +214,10 @@ export const SessionProvider = (props: { initialValue: {} as AuthToken }) + /** + * Checks if the current session has expired and refreshes the session if necessary. + * Sets up a timer to check the session expiration every minute. + */ const checkSessionIsExpired = () => { const expires_at_data = localStorage?.getItem('expires_at') @@ -209,9 +236,11 @@ export const SessionProvider = (props: { } } - onCleanup(() => clearTimeout(minuteLater as NodeJS.Timeout)) + onCleanup(() => { + if (minuteLater) clearTimeout(minuteLater) + }) - // initial effect + // Initial effect onMount(() => { setConfig({ ...defaultConfig, @@ -221,16 +250,23 @@ export const SessionProvider = (props: { loadSession() }) - // callback state updater + // Callback state updater createEffect( on([() => props.onStateChangeCallback, session], ([_, ses]) => { - ses?.user?.id && props.onStateChangeCallback(ses) + if (ses?.user?.id) props.onStateChangeCallback(ses) }) ) const [authCallback, setAuthCallback] = createSignal<() => void>(noop) + + /** + * Requires the user to be authenticated before executing a callback function. + * If the user is not authenticated, it shows the authentication modal. + * @param callback - The function to execute after authentication. + * @param modalSource - The source of the authentication modal. + */ const requireAuthentication = (callback: () => void, modalSource: AuthModalSource) => { - setAuthCallback((_cb) => callback) + setAuthCallback(() => callback) if (!session()) { loadSession() if (!session()) { @@ -243,23 +279,36 @@ export const SessionProvider = (props: { const handler = authCallback() if (handler !== noop) { handler() - setAuthCallback((_cb) => noop) + setAuthCallback(() => noop) } }) - // authorizer api proxy methods + /** + * General function to authenticate a user using a specified authentication function. + * @param authFunction - The authentication function to use (e.g., signup, login). + * @param params - The parameters to pass to the authentication function. + * @returns An object containing data and errors from the authentication attempt. + */ + type AuthFunctionType = ( + data: SignupInput | LoginInput | UpdateProfileInput + ) => Promise> const authenticate = async ( - authFunction: (data: SignupInput) => Promise>, - // biome-ignore lint/suspicious/noExplicitAny: authorizer - params: any + authFunction: AuthFunctionType, + params: SignupInput | LoginInput | UpdateProfileInput ) => { const resp = await authFunction(params) console.debug('[context.session] authenticate:', resp) if (resp?.data && resp?.errors.length === 0) setSession(resp.data as AuthToken) return { data: resp?.data, errors: resp?.errors } } + + /** + * Signs up a new user using the provided parameters. + * @param params - The signup input parameters. + * @returns A Promise resolving to `true` if signup was successful, otherwise `false`. + */ const signUp = async (params: SignupInput): Promise => { - const resp = await authenticate(authorizer().signup, params as SignupInput) + const resp = await authenticate(authorizer().signup as AuthFunctionType, params as SignupInput) console.debug('[context.session] signUp:', resp) if (resp?.data) { setSession(resp.data as AuthToken) @@ -268,8 +317,13 @@ export const SessionProvider = (props: { return false } + /** + * Signs in a user using the provided credentials. + * @param params - The login input parameters. + * @returns A Promise resolving to `true` if sign-in was successful, otherwise `false`. + */ const signIn = async (params: LoginInput): Promise => { - const resp = await authenticate(authorizer().login, params as LoginInput) + const resp = await authenticate(authorizer().login as AuthFunctionType, params) console.debug('[context.session] signIn:', resp) if (resp?.data) { setSession(resp.data as AuthToken) @@ -280,61 +334,97 @@ export const SessionProvider = (props: { return false } - const updateProfile = async (params: UpdateProfileInput) => { - const resp = await authenticate(authorizer().updateProfile, params as UpdateProfileInput) + /** + * Updates the user's profile with the provided parameters. + * @param params - The update profile input parameters. + * @returns A Promise resolving to `true` if the update was successful, otherwise `false`. + */ + const updateProfile = async (params: UpdateProfileInput): Promise => { + const resp = await authenticate(authorizer().updateProfile, params) console.debug('[context.session] updateProfile response:', resp) if (resp?.data) { - // console.debug('[context.session] response data ', resp.data) - // FIXME: renew updated profile + // Optionally refresh session or user data here return true } return false } - const signOut = async () => { + /** + * Signs out the current user and clears the session. + * @returns A Promise resolving to `true` if sign-out was successful. + */ + const signOut = async (): Promise => { const authResult: ApiResponse = await authorizer().logout() - // console.debug('[context.session] sign out', authResult) if (authResult) { setSession({} as AuthToken) setIsSessionLoaded(true) showSnackbar({ body: t("You've successfully logged out") }) - // console.debug(session()) return true } return false } - const changePassword = async (password: string, token: string) => { + /** + * Changes the user's password using a token from a password reset email. + * @param password - The new password. + * @param token - The token from the password reset email. + * @returns A Promise resolving to `true` if the password was changed successfully. + */ + const changePassword = async (password: string, token: string): Promise => { const resp = await authorizer().resetPassword({ password, token, confirm_password: password }) console.debug('[context.session] change password response:', resp) + if (resp.data) { + return true + } + return false } - const forgotPassword = async (params: ForgotPasswordInput) => { + /** + * Initiates the forgot password process for the given email. + * @param params - The forgot password input parameters. + * @returns A Promise resolving to an error message if any, otherwise an empty string. + */ + const forgotPassword = async (params: ForgotPasswordInput): Promise => { const resp = await authorizer().forgotPassword(params) - console.debug('[context.session] change password response:', resp) - return resp?.errors?.pop()?.message || '' + console.debug('[context.session] forgot password response:', resp) + if (resp.errors.length > 0) { + return resp.errors.pop()?.message || '' + } + return '' } + /** + * Resends the verification email to the user. + * @param params - The resend verify email input parameters. + * @returns A Promise resolving to `true` if the email was sent successfully. + */ const resendVerifyEmail = async (params: ResendVerifyEmailInput): Promise => { - const resp = await authorizer().resendVerifyEmail(params as ResendVerifyEmailInput) + const resp = await authorizer().resendVerifyEmail(params) console.debug('[context.session] resend verify email response:', resp) - if (resp.errors) { + if (resp.errors.length > 0) { resp.errors.forEach((error) => { showSnackbar({ type: 'error', body: error.message }) }) + return false } - return resp ? resp.data?.message === 'Verification email has been sent. Please check your inbox' : false + return resp.data?.message === 'Verification email has been sent. Please check your inbox' } + /** + * Checks if an email is already registered. + * @param email - The email to check. + * @returns A Promise resolving to the message from the server indicating the registration status. + */ const isRegistered = async (email: string): Promise => { console.debug('[context.session] calling is_registered for ', email) try { const response = await authorizer().graphqlQuery({ - query: `query { is_registered(email: "${email}") { message }}` + query: 'query IsRegistered($email: String!) { is_registered(email: $email) { message }}', + variables: { email } }) return response?.data?.is_registered?.message } catch (error) { @@ -361,6 +451,16 @@ export const SessionProvider = (props: { console.warn(error) } } + + // authorized graphql client + const [client, setClient] = createSignal() + createEffect( + on(session, (s?: AuthToken) => { + const tkn = s?.access_token + setClient((_c?: Client) => graphqlClientCreate(coreApiUrl, tkn)) + }) + ) + const actions = { loadSession, requireAuthentication, @@ -378,6 +478,7 @@ export const SessionProvider = (props: { isRegistered } const value: SessionContextType = { + client, authError, config, session, diff --git a/src/intl/locales/ru/translation.json b/src/intl/locales/ru/translation.json index 4e0119a1..4169bc12 100644 --- a/src/intl/locales/ru/translation.json +++ b/src/intl/locales/ru/translation.json @@ -295,6 +295,7 @@ "New stories and more are waiting for you every day!": "Каждый день вас ждут новые истории и ещё много всего интересного!", "Newsletter": "Рассылка", "Night mode": "Ночная тема", + "No drafts": "Нет черновиков", "No notifications yet": "Уведомлений пока нет", "No such account, please try to register": "Такой адрес не найден, попробуйте зарегистрироваться", "not verified": "ещё не подтверждён", diff --git a/src/routes/edit/(drafts).tsx b/src/routes/edit/(drafts).tsx index 3f8b4ca9..919f04f0 100644 --- a/src/routes/edit/(drafts).tsx +++ b/src/routes/edit/(drafts).tsx @@ -1,13 +1,10 @@ import { createAsync } from '@solidjs/router' import { Client } from '@urql/core' -import { createMemo } from 'solid-js' import { AuthGuard } from '~/components/AuthGuard' import { DraftsView } from '~/components/Views/DraftsView' import { PageLayout } from '~/components/_shared/PageLayout' -import { coreApiUrl } from '~/config' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' -import { graphqlClientCreate } from '~/graphql/client' import getDraftsQuery from '~/graphql/query/core/articles-load-drafts' import { Shout } from '~/graphql/schema/core.gen' @@ -19,9 +16,8 @@ const fetchDrafts = async (client: Client) => { export default () => { const { t } = useLocalize() - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) - const drafts = createAsync(async () => await fetchDrafts(client())) + const { client } = useSession() + const drafts = createAsync(async () => client() && (await fetchDrafts(client() as Client))) return ( diff --git a/src/routes/edit/[id]/(draft).tsx b/src/routes/edit/[id]/(draft).tsx index d494ce48..52bdd6ca 100644 --- a/src/routes/edit/[id]/(draft).tsx +++ b/src/routes/edit/[id]/(draft).tsx @@ -2,11 +2,9 @@ import { RouteSectionProps, redirect } from '@solidjs/router' import { createEffect, createMemo, createSignal, lazy, on } from 'solid-js' import { AuthGuard } from '~/components/AuthGuard' import { PageLayout } from '~/components/_shared/PageLayout' -import { coreApiUrl } from '~/config' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' import { useSnackbar } from '~/context/ui' -import { graphqlClientCreate } from '~/graphql/client' import getShoutDraft from '~/graphql/query/core/article-my' import { Shout } from '~/graphql/schema/core.gen' import { LayoutType } from '~/types/common' @@ -15,31 +13,32 @@ const EditView = lazy(() => import('~/components/Views/EditView/EditView')) export default (props: RouteSectionProps) => { const { t } = useLocalize() - const { session } = useSession() + const { session, client } = useSession() const snackbar = useSnackbar() - const fail = async (error: string) => { - console.error(error) - const errorMessage = error === 'forbidden' ? "You can't edit this post" : error - await snackbar?.showSnackbar({ type: 'error', body: t(errorMessage) }) - redirect('/edit') // all drafts page - } const [shout, setShout] = createSignal() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) - createEffect(on(session, (s) => s?.access_token && loadDraft(), { defer: true })) - - const loadDraft = async () => { - const shout_id = Number.parseInt(props.params.id) - const result = await client()?.query(getShoutDraft, { shout_id }).toPromise() - if (result) { - const { shout: loadedShout, error } = result.data.get_my_shout - if (error) { - fail(error) - } else { - setShout(loadedShout) - } - } - } + createEffect( + on( + session, + async (s) => { + if (!s?.access_token) return + const shout_id = Number.parseInt(props.params.id) + const result = await client()?.query(getShoutDraft, { shout_id }).toPromise() + if (result) { + const { shout: loadedShout, error } = result.data.get_my_shout + if (error) { + console.error(error) + const errorMessage = error === 'forbidden' ? "You can't edit this post" : error + await snackbar?.showSnackbar({ type: 'error', body: t(errorMessage) }) + redirect('/edit') // all drafts page + } else { + setShout(loadedShout) + } + } + }, + {} + ) + ) const title = createMemo(() => { const layout = (shout()?.layout as LayoutType) || 'article' diff --git a/src/routes/edit/[id]/settings.tsx b/src/routes/edit/[id]/settings.tsx index 7610d02a..9f33d198 100644 --- a/src/routes/edit/[id]/settings.tsx +++ b/src/routes/edit/[id]/settings.tsx @@ -1,30 +1,36 @@ +import { AuthToken } from '@authorizerdev/authorizer-js' import { RouteSectionProps } from '@solidjs/router' -import { createEffect, createMemo, createSignal, on } from 'solid-js' +import { createEffect, createSignal, on } from 'solid-js' import { AuthGuard } from '~/components/AuthGuard' import EditSettingsView from '~/components/Views/EditView/EditSettingsView' import { PageLayout } from '~/components/_shared/PageLayout' -import { coreApiUrl } from '~/config' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' -import { graphqlClientCreate } from '~/graphql/client' import getShoutDraft from '~/graphql/query/core/article-my' import { Shout } from '~/graphql/schema/core.gen' export default (props: RouteSectionProps) => { const { t } = useLocalize() - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) - createEffect(on(session, (s) => s?.access_token && loadDraft(), { defer: true })) + const { session, client } = useSession() const [shout, setShout] = createSignal() - const loadDraft = async () => { - const shout_id = Number.parseInt(props.params.id) - const result = await client()?.query(getShoutDraft, { shout_id }).toPromise() - if (result) { - const { shout: loadedShout, error } = result.data.get_my_shout - if (error) throw new Error(error) - setShout(loadedShout) - } - } + + createEffect( + on( + session, + async (s?: AuthToken) => { + if (!s?.access_token) return + const shout_id = Number.parseInt(props.params.id) + const result = await client()?.query(getShoutDraft, { shout_id }).toPromise() + if (result) { + const { shout: loadedShout, error } = result.data.get_my_shout + if (error) throw new Error(error) + setShout(loadedShout) + } + }, + {} + ) + ) + return ( diff --git a/src/routes/edit/new.tsx b/src/routes/edit/new.tsx index 326d47ad..28d24947 100644 --- a/src/routes/edit/new.tsx +++ b/src/routes/edit/new.tsx @@ -1,25 +1,23 @@ import { useNavigate } from '@solidjs/router' import { clsx } from 'clsx' -import { For, createMemo } from 'solid-js' +import { For } from 'solid-js' import { AuthGuard } from '~/components/AuthGuard' import { Button } from '~/components/_shared/Button' import { Icon } from '~/components/_shared/Icon' import { PageLayout } from '~/components/_shared/PageLayout' -import { coreApiUrl } from '~/config' import { useEditorContext } from '~/context/editor' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' import { useSnackbar } from '~/context/ui' -import { graphqlClientCreate } from '~/graphql/client' import createShoutMutation from '~/graphql/mutation/core/article-create' -import styles from '~/styles/Create.module.scss' import { LayoutType } from '~/types/common' +import styles from '~/styles/Create.module.scss' + export default () => { const { t } = useLocalize() - const { session } = useSession() + const { client } = useSession() const { saveDraftToLocalStorage } = useEditorContext() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) const { showSnackbar } = useSnackbar() const navigate = useNavigate() diff --git a/src/routes/feed/my/[...mode]/[...order].tsx b/src/routes/feed/my/[...mode]/[...order].tsx index bc7ea929..4155cbf1 100644 --- a/src/routes/feed/my/[...mode]/[...order].tsx +++ b/src/routes/feed/my/[...mode]/[...order].tsx @@ -1,11 +1,11 @@ import { RouteSectionProps, useSearchParams } from '@solidjs/router' import { createEffect, createMemo } from 'solid-js' + import { AUTHORS_PER_PAGE } from '~/components/Views/AllAuthors/AllAuthors' import { Feed } from '~/components/Views/Feed' import { FeedProps } from '~/components/Views/Feed/Feed' import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper' import { PageLayout } from '~/components/_shared/PageLayout' -import { coreApiUrl } from '~/config' import { useFeed } from '~/context/feed' import { useLocalize } from '~/context/localize' import { ReactionsProvider } from '~/context/reactions' @@ -17,7 +17,6 @@ import { loadFollowedShouts, loadUnratedShouts } from '~/graphql/api/private' -import { graphqlClientCreate } from '~/graphql/client' import { LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen' import { FromPeriod, getFromDate } from '~/lib/fromPeriod' @@ -38,8 +37,7 @@ export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>) const [searchParams] = useSearchParams() // ?period=month const { t } = useLocalize() const { setFeed, feed } = useFeed() - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { client } = useSession() // preload all topics const { addTopics, sortedTopics } = useTopics()