graphql-client-unmemo
Some checks failed
deploy / test (push) Failing after 6m50s
deploy / Update templates on Mailgun (push) Has been skipped

This commit is contained in:
Untone 2024-09-24 12:15:50 +03:00
parent 29d1661993
commit 317d4a004c
23 changed files with 274 additions and 227 deletions

View File

@ -3,12 +3,10 @@ import { clsx } from 'clsx'
import { For, Show, Suspense, createMemo, createSignal, lazy } from 'solid-js' import { For, Show, Suspense, createMemo, createSignal, lazy } from 'solid-js'
import { Icon } from '~/components/_shared/Icon' import { Icon } from '~/components/_shared/Icon'
import { ShowIfAuthenticated } from '~/components/_shared/ShowIfAuthenticated' import { ShowIfAuthenticated } from '~/components/_shared/ShowIfAuthenticated'
import { coreApiUrl } from '~/config'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useReactions } from '~/context/reactions' import { useReactions } from '~/context/reactions'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { useSnackbar, useUI } from '~/context/ui' import { useSnackbar, useUI } from '~/context/ui'
import { graphqlClientCreate } from '~/graphql/client'
import deleteReactionMutation from '~/graphql/mutation/core/reaction-destroy' import deleteReactionMutation from '~/graphql/mutation/core/reaction-destroy'
import { import {
Author, Author,
@ -45,12 +43,11 @@ export const Comment = (props: Props) => {
const [editMode, setEditMode] = createSignal(false) const [editMode, setEditMode] = createSignal(false)
const [clearEditor, setClearEditor] = createSignal(false) const [clearEditor, setClearEditor] = createSignal(false)
const [editedBody, setEditedBody] = createSignal<string>() const [editedBody, setEditedBody] = createSignal<string>()
const { session } = useSession() const { session, client } = useSession()
const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author) const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
const { createShoutReaction, updateShoutReaction } = useReactions() const { createShoutReaction, updateShoutReaction } = useReactions()
const { showConfirm } = useUI() const { showConfirm } = useUI()
const { showSnackbar } = useSnackbar() const { showSnackbar } = useSnackbar()
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
const canEdit = createMemo( const canEdit = createMemo(
() => () =>
Boolean(author()?.id) && Boolean(author()?.id) &&

View File

@ -1,10 +1,8 @@
import type { Author } from '~/graphql/schema/core.gen' import type { Author } from '~/graphql/schema/core.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createMemo, createSignal } from 'solid-js' import { Show, createSignal } from 'solid-js'
import { coreApiUrl } from '~/config'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { graphqlClientCreate } from '~/graphql/client'
import rateAuthorMutation from '~/graphql/mutation/core/author-rate' import rateAuthorMutation from '~/graphql/mutation/core/author-rate'
import styles from './AuthorRatingControl.module.scss' import styles from './AuthorRatingControl.module.scss'
@ -17,8 +15,7 @@ export const AuthorRatingControl = (props: AuthorRatingControlProps) => {
const isUpvoted = false const isUpvoted = false
const isDownvoted = false const isDownvoted = false
const { session } = useSession() const { client } = useSession()
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
// eslint-disable-next-line unicorn/consistent-function-scoping // eslint-disable-next-line unicorn/consistent-function-scoping
const handleRatingChange = async (isUpvote: boolean) => { const handleRatingChange = async (isUpvote: boolean) => {

View File

@ -3,7 +3,6 @@ import { clsx } from 'clsx'
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on } from 'solid-js' import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on } from 'solid-js'
import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper' import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
import { Loading } from '~/components/_shared/Loading' import { Loading } from '~/components/_shared/Loading'
import { coreApiUrl } from '~/config'
import { useAuthors } from '~/context/authors' import { useAuthors } from '~/context/authors'
import { SHOUTS_PER_PAGE, useFeed } from '~/context/feed' import { SHOUTS_PER_PAGE, useFeed } from '~/context/feed'
import { useFollowing } from '~/context/following' import { useFollowing } from '~/context/following'
@ -11,7 +10,6 @@ import { useLocalize } from '~/context/localize'
import { useReactions } from '~/context/reactions' import { useReactions } from '~/context/reactions'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { loadReactions, loadShouts } from '~/graphql/api/public' import { loadReactions, loadShouts } from '~/graphql/api/public'
import { graphqlClientCreate } from '~/graphql/client'
import getAuthorFollowersQuery from '~/graphql/query/core/author-followers' import getAuthorFollowersQuery from '~/graphql/query/core/author-followers'
import getAuthorFollowsQuery from '~/graphql/query/core/author-follows' import getAuthorFollowsQuery from '~/graphql/query/core/author-follows'
import type { Author, Reaction, Shout, Topic } from '~/graphql/schema/core.gen' import type { Author, Reaction, Shout, Topic } from '~/graphql/schema/core.gen'
@ -45,8 +43,7 @@ export const AuthorView = (props: AuthorViewProps) => {
const params = useParams() const params = useParams()
const [currentTab, setCurrentTab] = createSignal<string>(params.tab) const [currentTab, setCurrentTab] = createSignal<string>(params.tab)
const { session } = useSession() const { session, client } = useSession()
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
const { loadAuthor, authorsEntities } = useAuthors() const { loadAuthor, authorsEntities } = useAuthors()
const { followers: myFollowers, follows: myFollows } = useFollowing() const { followers: myFollowers, follows: myFollows } = useFollowing()

View File

@ -1,17 +1,14 @@
import { useNavigate } from '@solidjs/router' import { useNavigate } from '@solidjs/router'
import { clsx } from 'clsx' 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 { Draft } from '~/components/Draft'
import { Loading } from '~/components/_shared/Loading'
import { useEditorContext } from '~/context/editor' import { useEditorContext } from '~/context/editor'
import { useSession } from '~/context/session' import { useLocalize } from '~/context/localize'
import { Shout } from '~/graphql/schema/core.gen' import { Shout } from '~/graphql/schema/core.gen'
import styles from './DraftsView.module.scss' import styles from './DraftsView.module.scss'
export const DraftsView = (props: { drafts: Shout[] }) => { export const DraftsView = (props: { drafts: Shout[] }) => {
const [drafts, setDrafts] = createSignal<Shout[]>(props.drafts || []) const [drafts, setDrafts] = createSignal<Shout[]>(props.drafts || [])
const { session } = useSession()
const authorized = createMemo<boolean>(() => Boolean(session()?.access_token))
const navigate = useNavigate() const navigate = useNavigate()
const { publishShoutById, deleteShout } = useEditorContext() const { publishShoutById, deleteShout } = useEditorContext()
const handleDraftDelete = async (shout: Shout) => { const handleDraftDelete = async (shout: Shout) => {
@ -26,13 +23,19 @@ export const DraftsView = (props: { drafts: Shout[] }) => {
setTimeout(() => navigate('/feed'), 2000) setTimeout(() => navigate('/feed'), 2000)
} }
const { t } = useLocalize()
return ( return (
<div class={clsx(styles.DraftsView)}> <div class={clsx(styles.DraftsView)}>
<Show when={authorized()} fallback={<Loading />}>
<div class="wide-container"> <div class="wide-container">
<div class="row offset-md-5">
<h2>{t('Drafts')}</h2>
</div>
<Show when={drafts()} fallback={t('No drafts')}>
{(ddd) => (
<div class="row"> <div class="row">
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5"> <div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">
<For each={drafts()}> <For each={ddd()}>
{(draft) => ( {(draft) => (
<Draft <Draft
class={styles.draft} class={styles.draft}
@ -44,8 +47,9 @@ export const DraftsView = (props: { drafts: Shout[] }) => {
</For> </For>
</div> </div>
</div> </div>
</div> )}
</Show> </Show>
</div> </div>
</div>
) )
} }

View File

@ -1,15 +1,13 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import deepEqual from 'fast-deep-equal' 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 { createStore } from 'solid-js/store'
import { debounce } from 'throttle-debounce' import { debounce } from 'throttle-debounce'
import { Icon } from '~/components/_shared/Icon' import { Icon } from '~/components/_shared/Icon'
import { InviteMembers } from '~/components/_shared/InviteMembers' import { InviteMembers } from '~/components/_shared/InviteMembers'
import { coreApiUrl } from '~/config'
import { ShoutForm, useEditorContext } from '~/context/editor' import { ShoutForm, useEditorContext } from '~/context/editor'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { graphqlClientCreate } from '~/graphql/client'
import getMyShoutQuery from '~/graphql/query/core/article-my' import getMyShoutQuery from '~/graphql/query/core/article-my'
import type { Shout, Topic } from '~/graphql/schema/core.gen' import type { Shout, Topic } from '~/graphql/schema/core.gen'
import { isDesktop } from '~/lib/mediaQuery' import { isDesktop } from '~/lib/mediaQuery'
@ -44,8 +42,7 @@ const handleScrollTopButtonClick = (ev: MouseEvent | TouchEvent) => {
export const EditSettingsView = (props: Props) => { export const EditSettingsView = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const [isScrolled, setIsScrolled] = createSignal(false) const [isScrolled, setIsScrolled] = createSignal(false)
const { session } = useSession() const { client } = useSession()
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
const { form, setForm, saveDraft, saveDraftToLocalStorage, getDraftFromLocalStorage } = useEditorContext() const { form, setForm, saveDraft, saveDraftToLocalStorage, getDraftFromLocalStorage } = useEditorContext()
const [shoutTopics, setShoutTopics] = createSignal<Topic[]>([]) const [shoutTopics, setShoutTopics] = createSignal<Topic[]>([])
const [draft, setDraft] = createSignal() const [draft, setDraft] = createSignal()

View File

@ -1,6 +1,6 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import deepEqual from 'fast-deep-equal' 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 { createStore } from 'solid-js/store'
import { debounce } from 'throttle-debounce' import { debounce } from 'throttle-debounce'
import { DropArea } from '~/components/_shared/DropArea' import { DropArea } from '~/components/_shared/DropArea'
@ -9,11 +9,9 @@ import { InviteMembers } from '~/components/_shared/InviteMembers'
import { Loading } from '~/components/_shared/Loading' import { Loading } from '~/components/_shared/Loading'
import { Popover } from '~/components/_shared/Popover' import { Popover } from '~/components/_shared/Popover'
import { EditorSwiper } from '~/components/_shared/SolidSwiper' import { EditorSwiper } from '~/components/_shared/SolidSwiper'
import { coreApiUrl } from '~/config'
import { ShoutForm, useEditorContext } from '~/context/editor' import { ShoutForm, useEditorContext } from '~/context/editor'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { graphqlClientCreate } from '~/graphql/client'
import getMyShoutQuery from '~/graphql/query/core/article-my' import getMyShoutQuery from '~/graphql/query/core/article-my'
import type { Shout, Topic } from '~/graphql/schema/core.gen' import type { Shout, Topic } from '~/graphql/schema/core.gen'
import { slugify } from '~/intl/translit' import { slugify } from '~/intl/translit'
@ -55,7 +53,7 @@ const handleScrollTopButtonClick = (ev: MouseEvent | TouchEvent) => {
export const EditView = (props: Props) => { export const EditView = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { session } = useSession() const { client } = useSession()
const { const {
form, form,
formErrors, formErrors,
@ -76,8 +74,6 @@ export const EditView = (props: Props) => {
const [draft, setDraft] = createSignal<Shout>(props.shout) const [draft, setDraft] = createSignal<Shout>(props.shout)
const [mediaItems, setMediaItems] = createSignal<MediaItem[]>([]) const [mediaItems, setMediaItems] = createSignal<MediaItem[]>([])
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
createEffect(() => setMediaItems(JSON.parse(form.media || '[]'))) createEffect(() => setMediaItems(JSON.parse(form.media || '[]')))
createEffect( createEffect(

View File

@ -5,12 +5,10 @@ import { ConditionalWrapper } from '~/components/_shared/ConditionalWrapper'
import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper' import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
import { Loading } from '~/components/_shared/Loading' import { Loading } from '~/components/_shared/Loading'
import { ArticleCardSwiper } from '~/components/_shared/SolidSwiper/ArticleCardSwiper' import { ArticleCardSwiper } from '~/components/_shared/SolidSwiper/ArticleCardSwiper'
import { coreApiUrl } from '~/config'
import { EXPO_LAYOUTS, SHOUTS_PER_PAGE, useFeed } from '~/context/feed' import { EXPO_LAYOUTS, SHOUTS_PER_PAGE, useFeed } from '~/context/feed'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { loadShouts } from '~/graphql/api/public' import { loadShouts } from '~/graphql/api/public'
import { graphqlClientCreate } from '~/graphql/client'
import getRandomTopShoutsQuery from '~/graphql/query/core/articles-load-random-top' import getRandomTopShoutsQuery from '~/graphql/query/core/articles-load-random-top'
import { LoadShoutsFilters, LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' import { LoadShoutsFilters, LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen'
import { LayoutType } from '~/types/common' import { LayoutType } from '~/types/common'
@ -31,8 +29,7 @@ const LOAD_MORE_PAGE_SIZE = 12
export const Expo = (props: Props) => { export const Expo = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { session } = useSession() const { client } = useSession()
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
const [favoriteTopArticles, setFavoriteTopArticles] = createSignal<Shout[]>([]) const [favoriteTopArticles, setFavoriteTopArticles] = createSignal<Shout[]>([])
const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([]) const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([])

View File

@ -7,7 +7,6 @@ import { Icon } from '~/components/_shared/Icon'
import { InviteMembers } from '~/components/_shared/InviteMembers' import { InviteMembers } from '~/components/_shared/InviteMembers'
import { Loading } from '~/components/_shared/Loading' import { Loading } from '~/components/_shared/Loading'
import { ShareModal } from '~/components/_shared/ShareModal' import { ShareModal } from '~/components/_shared/ShareModal'
import { coreApiUrl } from '~/config'
import { useAuthors } from '~/context/authors' import { useAuthors } from '~/context/authors'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useReactions } from '~/context/reactions' import { useReactions } from '~/context/reactions'
@ -15,7 +14,6 @@ import { useSession } from '~/context/session'
import { useTopics } from '~/context/topics' import { useTopics } from '~/context/topics'
import { useUI } from '~/context/ui' import { useUI } from '~/context/ui'
import { loadUnratedShouts } from '~/graphql/api/private' import { loadUnratedShouts } from '~/graphql/api/private'
import { graphqlClientCreate } from '~/graphql/client'
import type { Author, Reaction, Shout } from '~/graphql/schema/core.gen' import type { Author, Reaction, Shout } from '~/graphql/schema/core.gen'
import { FeedSearchParams } from '~/routes/feed/[...order]' import { FeedSearchParams } from '~/routes/feed/[...order]'
import { byCreated } from '~/utils/sort' import { byCreated } from '~/utils/sort'
@ -49,11 +47,10 @@ const PERIODS = {
export const FeedView = (props: FeedProps) => { export const FeedView = (props: FeedProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const loc = useLocation() const loc = useLocation()
const { session } = useSession() const { client, session } = useSession()
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
const unrated = createAsync(async () => { const unrated = createAsync(async () => {
if (client) { if (client()) {
const shoutsLoader = loadUnratedShouts(client(), { limit: 5 }) const shoutsLoader = loadUnratedShouts(client(), { limit: 5 })
return await shoutsLoader() return await shoutsLoader()
} }

View File

@ -1,9 +1,17 @@
export const isDev = import.meta.env.MODE === 'development'
export const cdnUrl = 'https://cdn.discours.io' export const cdnUrl = 'https://cdn.discours.io'
export const thumborUrl = import.meta.env.PUBLIC_THUMBOR_URL || 'https://images.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 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 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 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 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 || ''
: ''

View File

@ -1,17 +1,15 @@
import { useMatch, useNavigate } from '@solidjs/router' import { useMatch, useNavigate } from '@solidjs/router'
import { Editor, EditorOptions } from '@tiptap/core' import { Editor, EditorOptions } from '@tiptap/core'
import type { JSX } from 'solid-js' 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 { SetStoreFunction, createStore } from 'solid-js/store'
import { createTiptapEditor } from 'solid-tiptap' import { createTiptapEditor } from 'solid-tiptap'
import { coreApiUrl } from '~/config'
import { useSnackbar } from '~/context/ui' import { useSnackbar } from '~/context/ui'
import deleteShoutQuery from '~/graphql/mutation/core/article-delete' import deleteShoutQuery from '~/graphql/mutation/core/article-delete'
import updateShoutQuery from '~/graphql/mutation/core/article-update' import updateShoutQuery from '~/graphql/mutation/core/article-update'
import { Topic, TopicInput } from '~/graphql/schema/core.gen' import { Topic, TopicInput } from '~/graphql/schema/core.gen'
import { slugify } from '~/intl/translit' import { slugify } from '~/intl/translit'
import { useFeed } from '../context/feed' import { useFeed } from '../context/feed'
import { graphqlClientCreate } from '../graphql/client'
import { useLocalize } from './localize' import { useLocalize } from './localize'
import { useSession } from './session' import { useSession } from './session'
@ -85,8 +83,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
const navigate = useNavigate() const navigate = useNavigate()
const matchEdit = useMatch(() => '/edit') const matchEdit = useMatch(() => '/edit')
const matchEditSettings = useMatch(() => '/editSettings') const matchEditSettings = useMatch(() => '/editSettings')
const { session } = useSession() const { client } = useSession()
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
const [editor, setEditor] = createSignal<Editor | undefined>() const [editor, setEditor] = createSignal<Editor | undefined>()
const { addFeed } = useFeed() const { addFeed } = useFeed()
const snackbar = useSnackbar() const snackbar = useSnackbar()

View File

@ -1,7 +1,6 @@
import { createLazyMemo } from '@solid-primitives/memo' import { createLazyMemo } from '@solid-primitives/memo'
import { makePersisted } from '@solid-primitives/storage' import { makePersisted } from '@solid-primitives/storage'
import { Accessor, JSX, Setter, createContext, createMemo, createSignal, useContext } from 'solid-js' import { Accessor, JSX, Setter, createContext, createSignal, useContext } from 'solid-js'
import { coreApiUrl } from '~/config'
import { loadFollowedShouts } from '~/graphql/api/private' import { loadFollowedShouts } from '~/graphql/api/private'
import { loadShoutsSearch as fetchShoutsSearch, getShout, loadShouts } from '~/graphql/api/public' import { loadShoutsSearch as fetchShoutsSearch, getShout, loadShouts } from '~/graphql/api/public'
import { import {
@ -12,7 +11,6 @@ import {
Topic Topic
} from '~/graphql/schema/core.gen' } from '~/graphql/schema/core.gen'
import { LayoutType } from '~/types/common' import { LayoutType } from '~/types/common'
import { graphqlClientCreate } from '../graphql/client'
import { byStat } from '../utils/sort' import { byStat } from '../utils/sort'
import { useSession } from './session' import { useSession } from './session'
@ -176,8 +174,7 @@ export const FeedProvider = (props: { children: JSX.Element }) => {
addFeed(result) addFeed(result)
return { hasMore, newShouts: result } return { hasMore, newShouts: result }
} }
const { session } = useSession() const { client } = useSession()
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
// Load the user's feed based on the provided options and update the articleEntities and sortedFeed state // Load the user's feed based on the provided options and update the articleEntities and sortedFeed state
const loadMyFeed = async ( const loadMyFeed = async (

View File

@ -1,21 +1,10 @@
import { import { Accessor, JSX, createContext, createEffect, createSignal, on, useContext } from 'solid-js'
Accessor,
JSX,
createContext,
createEffect,
createMemo,
createSignal,
on,
useContext
} from 'solid-js'
import { createStore } from 'solid-js/store' import { createStore } from 'solid-js/store'
import { coreApiUrl } from '~/config'
import followMutation from '~/graphql/mutation/core/follow' import followMutation from '~/graphql/mutation/core/follow'
import unfollowMutation from '~/graphql/mutation/core/unfollow' import unfollowMutation from '~/graphql/mutation/core/unfollow'
import loadAuthorFollowers from '~/graphql/query/core/author-followers' import loadAuthorFollowers from '~/graphql/query/core/author-followers'
import { Author, Community, FollowingEntity, Topic } from '~/graphql/schema/core.gen' import { Author, Community, FollowingEntity, Topic } from '~/graphql/schema/core.gen'
import { graphqlClientCreate } from '../graphql/client'
import { useSession } from './session' import { useSession } from './session'
export type FollowsFilter = 'all' | 'authors' | 'topics' | 'communities' export type FollowsFilter = 'all' | 'authors' | 'topics' | 'communities'
@ -70,9 +59,7 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
const [loading, setLoading] = createSignal<boolean>(false) const [loading, setLoading] = createSignal<boolean>(false)
const [followers, setFollowers] = createSignal<Author[]>([] as Author[]) const [followers, setFollowers] = createSignal<Author[]>([] as Author[])
const [follows, setFollows] = createStore<AuthorFollowsResult>(EMPTY_SUBSCRIPTIONS) const [follows, setFollows] = createStore<AuthorFollowsResult>(EMPTY_SUBSCRIPTIONS)
const { session } = useSession() const { session, client } = useSession()
const authorized = createMemo<boolean>(() => Boolean(session()?.access_token))
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
const fetchData = async () => { const fetchData = async () => {
setLoading(true) setLoading(true)
@ -96,7 +83,7 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
const [following, setFollowing] = createSignal<FollowingData>(defaultFollowing) const [following, setFollowing] = createSignal<FollowingData>(defaultFollowing)
const follow = async (what: FollowingEntity, slug: string) => { const follow = async (what: FollowingEntity, slug: string) => {
if (!authorized()) return if (!session()?.access_token) return
setFollowing({ slug, type: 'follow' }) setFollowing({ slug, type: 'follow' })
try { try {
const resp = await client()?.mutation(followMutation, { what, slug }).toPromise() 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) => { const unfollow = async (what: FollowingEntity, slug: string) => {
if (!authorized()) return if (!session()?.access_token) return
setFollowing({ slug: slug, type: 'unfollow' }) setFollowing({ slug: slug, type: 'unfollow' })
try { try {
const resp = await client()?.mutation(unfollowMutation, { what, slug }).toPromise() const resp = await client()?.mutation(unfollowMutation, { what, slug }).toPromise()

View File

@ -1,7 +1,5 @@
import type { Accessor, JSX } from 'solid-js' import type { Accessor, JSX } from 'solid-js'
import { createContext, createMemo, createSignal, useContext } from 'solid-js' import { createContext, createSignal, useContext } from 'solid-js'
import { chatApiUrl } from '~/config'
import { graphqlClientCreate } from '~/graphql/client'
import createChatMutation from '~/graphql/mutation/chat/chat-create' import createChatMutation from '~/graphql/mutation/chat/chat-create'
import createMessageMutation from '~/graphql/mutation/chat/chat-message-create' import createMessageMutation from '~/graphql/mutation/chat/chat-message-create'
import loadChatMessagesQuery from '~/graphql/query/chat/chat-messages-load-by' 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<Chat[]>([]) const [chats, setChats] = createSignal<Chat[]>([])
const [messages, setMessages] = createSignal<Message[]>([]) const [messages, setMessages] = createSignal<Message[]>([])
const { authorsSorted } = useAuthors() const { authorsSorted } = useAuthors()
const { session } = useSession() const { client } = useSession()
const client = createMemo(() => graphqlClientCreate(chatApiUrl, session()?.access_token))
const handleMessage = (sseMessage: SSEMessage) => { const handleMessage = (sseMessage: SSEMessage) => {
// handling all action types: create update delete join left seen // handling all action types: create update delete join left seen

View File

@ -1,12 +1,9 @@
import { makePersisted } from '@solid-primitives/storage' import { makePersisted } from '@solid-primitives/storage'
import type { Accessor, JSX } from 'solid-js' import type { Accessor, JSX } from 'solid-js'
import { createContext, createMemo, createSignal, onMount, useContext } from 'solid-js' import { createContext, createMemo, createSignal, onMount, useContext } from 'solid-js'
import { createStore } from 'solid-js/store' import { createStore } from 'solid-js/store'
import { Portal } from 'solid-js/web' import { Portal } from 'solid-js/web'
import { coreApiUrl } from '~/config'
import { graphqlClientCreate } from '~/graphql/client'
import markSeenMutation from '~/graphql/mutation/notifier/mark-seen' import markSeenMutation from '~/graphql/mutation/notifier/mark-seen'
import markSeenAfterMutation from '~/graphql/mutation/notifier/mark-seen-after' import markSeenAfterMutation from '~/graphql/mutation/notifier/mark-seen-after'
import markSeenThreadMutation from '~/graphql/mutation/notifier/mark-seen-thread' 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 [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0)
const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0) const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0)
const [notificationEntities, setNotificationEntities] = createStore<Record<string, NotificationGroup>>({}) const [notificationEntities, setNotificationEntities] = createStore<Record<string, NotificationGroup>>({})
const { session } = useSession() const { session, client } = useSession()
const authorized = createMemo<boolean>(() => Boolean(session()?.access_token))
const { addHandler } = useConnect() const { addHandler } = useConnect()
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
const loadNotificationsGrouped = async (options: QueryLoad_NotificationsArgs) => { const loadNotificationsGrouped = async (options: QueryLoad_NotificationsArgs) => {
if (authorized()) { if (session()?.access_token) {
const resp = await client()?.query(getNotifications, options).toPromise() const resp = await client()?.query(getNotifications, options).toPromise()
const result = resp?.data?.get_notifications const result = resp?.data?.get_notifications
const groups = result?.notifications || [] const groups = result?.notifications || []
@ -87,7 +82,7 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
onMount(() => { onMount(() => {
addHandler((data: SSEMessage) => { addHandler((data: SSEMessage) => {
if (data.entity === 'reaction' && authorized()) { if (data.entity === 'reaction' && session()?.access_token) {
console.info('[context.notifications] event', data) console.info('[context.notifications] event', data)
loadNotificationsGrouped({ loadNotificationsGrouped({
after: after() || now, after: after() || now,
@ -107,14 +102,14 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
} }
const markSeenAll = async () => { const markSeenAll = async () => {
if (authorized()) { if (session()?.access_token) {
const _resp = await client()?.mutation(markSeenAfterMutation, { after: after() }).toPromise() const _resp = await client()?.mutation(markSeenAfterMutation, { after: after() }).toPromise()
await loadNotificationsGrouped({ after: after() || now, limit: loadedNotificationsCount() }) await loadNotificationsGrouped({ after: after() || now, limit: loadedNotificationsCount() })
} }
} }
const markSeen = async (notification_id: number) => { const markSeen = async (notification_id: number) => {
if (authorized()) { if (session()?.access_token) {
await client()?.mutation(markSeenMutation, { notification_id }).toPromise() await client()?.mutation(markSeenMutation, { notification_id }).toPromise()
await loadNotificationsGrouped({ after: after() || now, limit: loadedNotificationsCount() }) await loadNotificationsGrouped({ after: after() || now, limit: loadedNotificationsCount() })
} }

View File

@ -1,20 +1,9 @@
import type { Author, ProfileInput } from '~/graphql/schema/core.gen' import type { Author, ProfileInput } from '~/graphql/schema/core.gen'
import { AuthToken } from '@authorizerdev/authorizer-js' import { AuthToken } from '@authorizerdev/authorizer-js'
import { import { Accessor, JSX, createContext, createEffect, createSignal, on, useContext } from 'solid-js'
Accessor,
JSX,
createContext,
createEffect,
createMemo,
createSignal,
on,
useContext
} from 'solid-js'
import { createStore } from 'solid-js/store' import { createStore } from 'solid-js/store'
import { coreApiUrl } from '~/config'
import updateAuthorMuatation from '~/graphql/mutation/core/author-update' import updateAuthorMuatation from '~/graphql/mutation/core/author-update'
import { graphqlClientCreate } from '../graphql/client'
import { useAuthors } from './authors' import { useAuthors } from './authors'
import { useSession } from './session' import { useSession } from './session'
@ -41,8 +30,7 @@ const userpicUrl = (userpic: string) => {
} }
export const ProfileProvider = (props: { children: JSX.Element }) => { export const ProfileProvider = (props: { children: JSX.Element }) => {
const { session } = useSession() const { session, client } = useSession()
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
const { addAuthor } = useAuthors() const { addAuthor } = useAuthors()
const [form, setForm] = createStore<ProfileInput>({} as ProfileInput) const [form, setForm] = createStore<ProfileInput>({} as ProfileInput)
const [author, setAuthor] = createSignal<Author>({} as Author) const [author, setAuthor] = createSignal<Author>({} as Author)
@ -66,7 +54,7 @@ export const ProfileProvider = (props: { children: JSX.Element }) => {
const submit = async (profile: ProfileInput) => { const submit = async (profile: ProfileInput) => {
const response = await client()?.mutation(updateAuthorMuatation, profile).toPromise() const response = await client()?.mutation(updateAuthorMuatation, profile).toPromise()
if (response.error) { if (response?.error) {
console.error(response.error) console.error(response.error)
throw response.error throw response.error
} }

View File

@ -1,6 +1,5 @@
import type { Accessor, JSX } from 'solid-js' import type { Accessor, JSX } from 'solid-js'
import { createContext, createMemo, createSignal, onCleanup, useContext } from 'solid-js' import { createContext, createSignal, onCleanup, useContext } from 'solid-js'
import { coreApiUrl } from '~/config'
import { loadReactions } from '~/graphql/api/public' import { loadReactions } from '~/graphql/api/public'
import createReactionMutation from '~/graphql/mutation/core/reaction-create' import createReactionMutation from '~/graphql/mutation/core/reaction-create'
import destroyReactionMutation from '~/graphql/mutation/core/reaction-destroy' import destroyReactionMutation from '~/graphql/mutation/core/reaction-destroy'
@ -12,7 +11,6 @@ import {
Reaction, Reaction,
ReactionKind ReactionKind
} from '~/graphql/schema/core.gen' } from '~/graphql/schema/core.gen'
import { graphqlClientCreate } from '../graphql/client'
import { useLocalize } from './localize' import { useLocalize } from './localize'
import { useSession } from './session' import { useSession } from './session'
import { useSnackbar } from './ui' import { useSnackbar } from './ui'
@ -41,8 +39,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
const [commentsByAuthor, setCommentsByAuthor] = createSignal<Record<number, Reaction[]>>({}) const [commentsByAuthor, setCommentsByAuthor] = createSignal<Record<number, Reaction[]>>({})
const { t } = useLocalize() const { t } = useLocalize()
const { showSnackbar } = useSnackbar() const { showSnackbar } = useSnackbar()
const { session } = useSession() const { client } = useSession()
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
const addShoutReactions = (rrr: Reaction[]) => { const addShoutReactions = (rrr: Reaction[]) => {
const newReactionEntities = { ...reactionEntities() } const newReactionEntities = { ...reactionEntities() }

View File

@ -12,6 +12,7 @@ import {
VerifyEmailInput VerifyEmailInput
} from '@authorizerdev/authorizer-js' } from '@authorizerdev/authorizer-js'
import { useSearchParams } from '@solidjs/router' import { useSearchParams } from '@solidjs/router'
import { Client } from '@urql/core'
import type { Accessor, JSX, Resource } from 'solid-js' import type { Accessor, JSX, Resource } from 'solid-js'
import { import {
createContext, createContext,
@ -25,13 +26,14 @@ import {
useContext useContext
} from 'solid-js' } from 'solid-js'
import { type AuthModalSource, useSnackbar, useUI } from '~/context/ui' 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' import { useLocalize } from './localize'
const defaultConfig: ConfigType = { const defaultConfig: ConfigType = {
authorizerURL: authApiUrl.replace('/graphql', ''), authorizerURL: authApiUrl.replace('/graphql', ''),
redirectURL: 'https://testing.discours.io', redirectURL: authorizerRedirectUrl,
clientID: 'b9038a34-ca59-41ae-a105-c7fbea603e24' clientID: authorizerClientId
} }
export type SessionContextType = { export type SessionContextType = {
@ -51,20 +53,22 @@ export type SessionContextType = {
signOut: () => Promise<boolean> signOut: () => Promise<boolean>
oauth: (provider: string) => Promise<void> oauth: (provider: string) => Promise<void>
forgotPassword: (params: ForgotPasswordInput) => Promise<string> forgotPassword: (params: ForgotPasswordInput) => Promise<string>
changePassword: (password: string, token: string) => void changePassword: (password: string, token: string) => Promise<boolean>
confirmEmail: (input: VerifyEmailInput) => Promise<void> confirmEmail: (input: VerifyEmailInput) => Promise<void>
setIsSessionLoaded: (loaded: boolean) => void setIsSessionLoaded: (loaded: boolean) => void
authorizer: () => Authorizer authorizer: () => Authorizer
isRegistered: (email: string) => Promise<string> isRegistered: (email: string) => Promise<string>
resendVerifyEmail: (params: ResendVerifyEmailInput) => Promise<boolean> resendVerifyEmail: (params: ResendVerifyEmailInput) => Promise<boolean>
client: Accessor<Client | undefined>
} }
const noop = () => null const noop = () => null
const metaRes = { const metaRes = {
data: { data: {
meta: { meta: {
version: 'latest', version: 'latest',
client_id: 'b9038a34-ca59-41ae-a105-c7fbea603e24', client_id: authorizerClientId,
is_google_login_enabled: true, is_google_login_enabled: true,
is_facebook_login_enabled: true, is_facebook_login_enabled: true,
is_github_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<SessionContextType>({} as SessionContextType) export const SessionContext = createContext<SessionContextType>({} as SessionContextType)
export function useSession() { export function useSession() {
return useContext(SessionContext) 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: { export const SessionProvider = (props: {
onStateChangeCallback(state: AuthToken): unknown onStateChangeCallback(state: AuthToken): unknown
children: JSX.Element children: JSX.Element
@ -113,45 +126,55 @@ export const SessionProvider = (props: {
const authorizer = createMemo(() => new Authorizer(config())) const authorizer = createMemo(() => new Authorizer(config()))
const [oauthState, setOauthState] = createSignal<string>() const [oauthState, setOauthState] = createSignal<string>()
// load // Session expiration timer
let minuteLater: NodeJS.Timeout | null let minuteLater: ReturnType<typeof setTimeout> | null = null
const [isSessionLoaded, setIsSessionLoaded] = createSignal(false) const [isSessionLoaded, setIsSessionLoaded] = createSignal(false)
const [authError, setAuthError] = createSignal<string>('') const [authError, setAuthError] = createSignal<string>('')
const { showModal } = useUI() const { showModal } = useUI()
// handle auth state callback from outside // Handle auth state callback from outside
onMount(() => { onMount(() => {
const params = searchParams const params = searchParams
if (params?.state) { if (params?.state) {
setOauthState((_s) => params?.state) setOauthState(params.state)
const scope = params?.scope ? params?.scope?.toString().split(' ') : ['openid', 'profile', 'email'] const scope = params.scope ? params.scope.toString().split(' ') : ['openid', 'profile', 'email']
if (scope) console.info(`[context.session] scope: ${scope}`) 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] })) setConfig((c: ConfigType) => ({ ...c, redirectURL: url.split('?')[0] }))
changeSearchParams({ mode: 'confirm-email', m: 'auth' }, { replace: true }) changeSearchParams({ mode: 'confirm-email', m: 'auth' }, { replace: true })
} }
}) })
// handle token confirm // Handle token confirmation
createEffect(() => { createEffect(() => {
const token = searchParams?.token const token = searchParams?.token
const access_token = searchParams?.access_token const access_token = searchParams?.access_token
if (access_token) if (access_token) {
changeSearchParams({ changeSearchParams(
{
mode: 'confirm-email', mode: 'confirm-email',
m: 'auth', m: 'auth',
access_token access_token
}) },
else if (token) { { replace: true }
changeSearchParams({ )
} else if (token) {
changeSearchParams(
{
mode: 'change-password', mode: 'change-password',
m: 'auth', m: 'auth',
token 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 () => { const sessionData = async () => {
try { try {
const s: ApiResponse<AuthToken> = await authorizer().getSession() const s: ApiResponse<AuthToken> = await authorizer().getSession()
@ -191,6 +214,10 @@ export const SessionProvider = (props: {
initialValue: {} as AuthToken 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 checkSessionIsExpired = () => {
const expires_at_data = localStorage?.getItem('expires_at') 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(() => { onMount(() => {
setConfig({ setConfig({
...defaultConfig, ...defaultConfig,
@ -221,16 +250,23 @@ export const SessionProvider = (props: {
loadSession() loadSession()
}) })
// callback state updater // Callback state updater
createEffect( createEffect(
on([() => props.onStateChangeCallback, session], ([_, ses]) => { on([() => props.onStateChangeCallback, session], ([_, ses]) => {
ses?.user?.id && props.onStateChangeCallback(ses) if (ses?.user?.id) props.onStateChangeCallback(ses)
}) })
) )
const [authCallback, setAuthCallback] = createSignal<() => void>(noop) 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) => { const requireAuthentication = (callback: () => void, modalSource: AuthModalSource) => {
setAuthCallback((_cb) => callback) setAuthCallback(() => callback)
if (!session()) { if (!session()) {
loadSession() loadSession()
if (!session()) { if (!session()) {
@ -243,23 +279,36 @@ export const SessionProvider = (props: {
const handler = authCallback() const handler = authCallback()
if (handler !== noop) { if (handler !== noop) {
handler() 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<ApiResponse<AuthToken | GenericResponse>>
const authenticate = async ( const authenticate = async (
authFunction: (data: SignupInput) => Promise<ApiResponse<AuthToken | GenericResponse>>, authFunction: AuthFunctionType,
// biome-ignore lint/suspicious/noExplicitAny: authorizer params: SignupInput | LoginInput | UpdateProfileInput
params: any
) => { ) => {
const resp = await authFunction(params) const resp = await authFunction(params)
console.debug('[context.session] authenticate:', resp) console.debug('[context.session] authenticate:', resp)
if (resp?.data && resp?.errors.length === 0) setSession(resp.data as AuthToken) if (resp?.data && resp?.errors.length === 0) setSession(resp.data as AuthToken)
return { data: resp?.data, errors: resp?.errors } 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<boolean> => { const signUp = async (params: SignupInput): Promise<boolean> => {
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) console.debug('[context.session] signUp:', resp)
if (resp?.data) { if (resp?.data) {
setSession(resp.data as AuthToken) setSession(resp.data as AuthToken)
@ -268,8 +317,13 @@ export const SessionProvider = (props: {
return false 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<boolean> => { const signIn = async (params: LoginInput): Promise<boolean> => {
const resp = await authenticate(authorizer().login, params as LoginInput) const resp = await authenticate(authorizer().login as AuthFunctionType, params)
console.debug('[context.session] signIn:', resp) console.debug('[context.session] signIn:', resp)
if (resp?.data) { if (resp?.data) {
setSession(resp.data as AuthToken) setSession(resp.data as AuthToken)
@ -280,61 +334,97 @@ export const SessionProvider = (props: {
return false 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<boolean> => {
const resp = await authenticate(authorizer().updateProfile, params)
console.debug('[context.session] updateProfile response:', resp) console.debug('[context.session] updateProfile response:', resp)
if (resp?.data) { if (resp?.data) {
// console.debug('[context.session] response data ', resp.data) // Optionally refresh session or user data here
// FIXME: renew updated profile
return true return true
} }
return false 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<boolean> => {
const authResult: ApiResponse<GenericResponse> = await authorizer().logout() const authResult: ApiResponse<GenericResponse> = await authorizer().logout()
// console.debug('[context.session] sign out', authResult)
if (authResult) { if (authResult) {
setSession({} as AuthToken) setSession({} as AuthToken)
setIsSessionLoaded(true) setIsSessionLoaded(true)
showSnackbar({ body: t("You've successfully logged out") }) showSnackbar({ body: t("You've successfully logged out") })
// console.debug(session())
return true return true
} }
return false 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<boolean> => {
const resp = await authorizer().resetPassword({ const resp = await authorizer().resetPassword({
password, password,
token, token,
confirm_password: password confirm_password: password
}) })
console.debug('[context.session] change password response:', resp) 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<string> => {
const resp = await authorizer().forgotPassword(params) const resp = await authorizer().forgotPassword(params)
console.debug('[context.session] change password response:', resp) console.debug('[context.session] forgot password response:', resp)
return resp?.errors?.pop()?.message || '' 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<boolean> => { const resendVerifyEmail = async (params: ResendVerifyEmailInput): Promise<boolean> => {
const resp = await authorizer().resendVerifyEmail(params as ResendVerifyEmailInput) const resp = await authorizer().resendVerifyEmail(params)
console.debug('[context.session] resend verify email response:', resp) console.debug('[context.session] resend verify email response:', resp)
if (resp.errors) { if (resp.errors.length > 0) {
resp.errors.forEach((error) => { resp.errors.forEach((error) => {
showSnackbar({ type: 'error', body: error.message }) 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<string> => { const isRegistered = async (email: string): Promise<string> => {
console.debug('[context.session] calling is_registered for ', email) console.debug('[context.session] calling is_registered for ', email)
try { try {
const response = await authorizer().graphqlQuery({ 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 return response?.data?.is_registered?.message
} catch (error) { } catch (error) {
@ -361,6 +451,16 @@ export const SessionProvider = (props: {
console.warn(error) console.warn(error)
} }
} }
// authorized graphql client
const [client, setClient] = createSignal<Client>()
createEffect(
on(session, (s?: AuthToken) => {
const tkn = s?.access_token
setClient((_c?: Client) => graphqlClientCreate(coreApiUrl, tkn))
})
)
const actions = { const actions = {
loadSession, loadSession,
requireAuthentication, requireAuthentication,
@ -378,6 +478,7 @@ export const SessionProvider = (props: {
isRegistered isRegistered
} }
const value: SessionContextType = { const value: SessionContextType = {
client,
authError, authError,
config, config,
session, session,

View File

@ -295,6 +295,7 @@
"New stories and more are waiting for you every day!": "Каждый день вас ждут новые истории и ещё много всего интересного!", "New stories and more are waiting for you every day!": "Каждый день вас ждут новые истории и ещё много всего интересного!",
"Newsletter": "Рассылка", "Newsletter": "Рассылка",
"Night mode": "Ночная тема", "Night mode": "Ночная тема",
"No drafts": "Нет черновиков",
"No notifications yet": "Уведомлений пока нет", "No notifications yet": "Уведомлений пока нет",
"No such account, please try to register": "Такой адрес не найден, попробуйте зарегистрироваться", "No such account, please try to register": "Такой адрес не найден, попробуйте зарегистрироваться",
"not verified": "ещё не подтверждён", "not verified": "ещё не подтверждён",

View File

@ -1,13 +1,10 @@
import { createAsync } from '@solidjs/router' import { createAsync } from '@solidjs/router'
import { Client } from '@urql/core' import { Client } from '@urql/core'
import { createMemo } from 'solid-js'
import { AuthGuard } from '~/components/AuthGuard' import { AuthGuard } from '~/components/AuthGuard'
import { DraftsView } from '~/components/Views/DraftsView' import { DraftsView } from '~/components/Views/DraftsView'
import { PageLayout } from '~/components/_shared/PageLayout' import { PageLayout } from '~/components/_shared/PageLayout'
import { coreApiUrl } from '~/config'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { graphqlClientCreate } from '~/graphql/client'
import getDraftsQuery from '~/graphql/query/core/articles-load-drafts' import getDraftsQuery from '~/graphql/query/core/articles-load-drafts'
import { Shout } from '~/graphql/schema/core.gen' import { Shout } from '~/graphql/schema/core.gen'
@ -19,9 +16,8 @@ const fetchDrafts = async (client: Client) => {
export default () => { export default () => {
const { t } = useLocalize() const { t } = useLocalize()
const { session } = useSession() const { client } = useSession()
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) const drafts = createAsync(async () => client() && (await fetchDrafts(client() as Client)))
const drafts = createAsync(async () => await fetchDrafts(client()))
return ( return (
<PageLayout title={`${t('Discours')} :: ${t('Drafts')}`}> <PageLayout title={`${t('Discours')} :: ${t('Drafts')}`}>

View File

@ -2,11 +2,9 @@ import { RouteSectionProps, redirect } from '@solidjs/router'
import { createEffect, createMemo, createSignal, lazy, on } from 'solid-js' import { createEffect, createMemo, createSignal, lazy, on } from 'solid-js'
import { AuthGuard } from '~/components/AuthGuard' import { AuthGuard } from '~/components/AuthGuard'
import { PageLayout } from '~/components/_shared/PageLayout' import { PageLayout } from '~/components/_shared/PageLayout'
import { coreApiUrl } from '~/config'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { useSnackbar } from '~/context/ui' import { useSnackbar } from '~/context/ui'
import { graphqlClientCreate } from '~/graphql/client'
import getShoutDraft from '~/graphql/query/core/article-my' import getShoutDraft from '~/graphql/query/core/article-my'
import { Shout } from '~/graphql/schema/core.gen' import { Shout } from '~/graphql/schema/core.gen'
import { LayoutType } from '~/types/common' import { LayoutType } from '~/types/common'
@ -15,31 +13,32 @@ const EditView = lazy(() => import('~/components/Views/EditView/EditView'))
export default (props: RouteSectionProps) => { export default (props: RouteSectionProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const { session } = useSession() const { session, client } = useSession()
const snackbar = useSnackbar() 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<Shout>() const [shout, setShout] = createSignal<Shout>()
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
createEffect(on(session, (s) => s?.access_token && loadDraft(), { defer: true })) createEffect(
on(
const loadDraft = async () => { session,
async (s) => {
if (!s?.access_token) return
const shout_id = Number.parseInt(props.params.id) const shout_id = Number.parseInt(props.params.id)
const result = await client()?.query(getShoutDraft, { shout_id }).toPromise() const result = await client()?.query(getShoutDraft, { shout_id }).toPromise()
if (result) { if (result) {
const { shout: loadedShout, error } = result.data.get_my_shout const { shout: loadedShout, error } = result.data.get_my_shout
if (error) { if (error) {
fail(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 { } else {
setShout(loadedShout) setShout(loadedShout)
} }
} }
} },
{}
)
)
const title = createMemo(() => { const title = createMemo(() => {
const layout = (shout()?.layout as LayoutType) || 'article' const layout = (shout()?.layout as LayoutType) || 'article'

View File

@ -1,22 +1,24 @@
import { AuthToken } from '@authorizerdev/authorizer-js'
import { RouteSectionProps } from '@solidjs/router' 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 { AuthGuard } from '~/components/AuthGuard'
import EditSettingsView from '~/components/Views/EditView/EditSettingsView' import EditSettingsView from '~/components/Views/EditView/EditSettingsView'
import { PageLayout } from '~/components/_shared/PageLayout' import { PageLayout } from '~/components/_shared/PageLayout'
import { coreApiUrl } from '~/config'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { graphqlClientCreate } from '~/graphql/client'
import getShoutDraft from '~/graphql/query/core/article-my' import getShoutDraft from '~/graphql/query/core/article-my'
import { Shout } from '~/graphql/schema/core.gen' import { Shout } from '~/graphql/schema/core.gen'
export default (props: RouteSectionProps) => { export default (props: RouteSectionProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const { session } = useSession() const { session, client } = useSession()
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
createEffect(on(session, (s) => s?.access_token && loadDraft(), { defer: true }))
const [shout, setShout] = createSignal<Shout>() const [shout, setShout] = createSignal<Shout>()
const loadDraft = async () => {
createEffect(
on(
session,
async (s?: AuthToken) => {
if (!s?.access_token) return
const shout_id = Number.parseInt(props.params.id) const shout_id = Number.parseInt(props.params.id)
const result = await client()?.query(getShoutDraft, { shout_id }).toPromise() const result = await client()?.query(getShoutDraft, { shout_id }).toPromise()
if (result) { if (result) {
@ -24,7 +26,11 @@ export default (props: RouteSectionProps) => {
if (error) throw new Error(error) if (error) throw new Error(error)
setShout(loadedShout) setShout(loadedShout)
} }
} },
{}
)
)
return ( return (
<PageLayout title={`${t('Discours')} :: ${t('Publication settings')}`}> <PageLayout title={`${t('Discours')} :: ${t('Publication settings')}`}>
<AuthGuard> <AuthGuard>

View File

@ -1,25 +1,23 @@
import { useNavigate } from '@solidjs/router' import { useNavigate } from '@solidjs/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, createMemo } from 'solid-js' import { For } from 'solid-js'
import { AuthGuard } from '~/components/AuthGuard' import { AuthGuard } from '~/components/AuthGuard'
import { Button } from '~/components/_shared/Button' import { Button } from '~/components/_shared/Button'
import { Icon } from '~/components/_shared/Icon' import { Icon } from '~/components/_shared/Icon'
import { PageLayout } from '~/components/_shared/PageLayout' import { PageLayout } from '~/components/_shared/PageLayout'
import { coreApiUrl } from '~/config'
import { useEditorContext } from '~/context/editor' import { useEditorContext } from '~/context/editor'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { useSnackbar } from '~/context/ui' import { useSnackbar } from '~/context/ui'
import { graphqlClientCreate } from '~/graphql/client'
import createShoutMutation from '~/graphql/mutation/core/article-create' import createShoutMutation from '~/graphql/mutation/core/article-create'
import styles from '~/styles/Create.module.scss'
import { LayoutType } from '~/types/common' import { LayoutType } from '~/types/common'
import styles from '~/styles/Create.module.scss'
export default () => { export default () => {
const { t } = useLocalize() const { t } = useLocalize()
const { session } = useSession() const { client } = useSession()
const { saveDraftToLocalStorage } = useEditorContext() const { saveDraftToLocalStorage } = useEditorContext()
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
const { showSnackbar } = useSnackbar() const { showSnackbar } = useSnackbar()
const navigate = useNavigate() const navigate = useNavigate()

View File

@ -1,11 +1,11 @@
import { RouteSectionProps, useSearchParams } from '@solidjs/router' import { RouteSectionProps, useSearchParams } from '@solidjs/router'
import { createEffect, createMemo } from 'solid-js' import { createEffect, createMemo } from 'solid-js'
import { AUTHORS_PER_PAGE } from '~/components/Views/AllAuthors/AllAuthors' import { AUTHORS_PER_PAGE } from '~/components/Views/AllAuthors/AllAuthors'
import { Feed } from '~/components/Views/Feed' import { Feed } from '~/components/Views/Feed'
import { FeedProps } from '~/components/Views/Feed/Feed' import { FeedProps } from '~/components/Views/Feed/Feed'
import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper' import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
import { PageLayout } from '~/components/_shared/PageLayout' import { PageLayout } from '~/components/_shared/PageLayout'
import { coreApiUrl } from '~/config'
import { useFeed } from '~/context/feed' import { useFeed } from '~/context/feed'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { ReactionsProvider } from '~/context/reactions' import { ReactionsProvider } from '~/context/reactions'
@ -17,7 +17,6 @@ import {
loadFollowedShouts, loadFollowedShouts,
loadUnratedShouts loadUnratedShouts
} from '~/graphql/api/private' } from '~/graphql/api/private'
import { graphqlClientCreate } from '~/graphql/client'
import { LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen' import { LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen'
import { FromPeriod, getFromDate } from '~/lib/fromPeriod' import { FromPeriod, getFromDate } from '~/lib/fromPeriod'
@ -38,8 +37,7 @@ export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>)
const [searchParams] = useSearchParams<FeedSearchParams>() // ?period=month const [searchParams] = useSearchParams<FeedSearchParams>() // ?period=month
const { t } = useLocalize() const { t } = useLocalize()
const { setFeed, feed } = useFeed() const { setFeed, feed } = useFeed()
const { session } = useSession() const { client } = useSession()
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
// preload all topics // preload all topics
const { addTopics, sortedTopics } = useTopics() const { addTopics, sortedTopics } = useTopics()