From 090a8f2633c44745d8935d54708b0213dc9ad468 Mon Sep 17 00:00:00 2001 From: Untone Date: Wed, 31 Jan 2024 15:34:15 +0300 Subject: [PATCH 01/14] dev --- .eslintrc.cjs | 7 +- .gitignore | 1 + package.json | 2 +- src/components/App.tsx | 26 ++-- .../Author/AuthorBadge/AuthorBadge.tsx | 65 +++------ .../Author/AuthorCard/AuthorCard.tsx | 75 ++++------ src/components/Feed/Row2.tsx | 42 ++---- src/components/Feed/Sidebar/Sidebar.tsx | 23 ++- src/components/Nav/Header/Header.tsx | 6 +- .../ProfileSettings/ProfileSettings.tsx | 2 +- src/components/Topic/Card.tsx | 63 ++++----- src/components/Topic/Full.tsx | 50 ++++--- .../Topic/TopicBadge/TopicBadge.tsx | 49 +++---- src/components/Views/AllTopics.tsx | 8 +- src/components/Views/Author/Author.tsx | 133 +++++++++++------- .../ProfileSubscriptions.tsx | 11 +- .../Views/PublishSettings/PublishSettings.tsx | 63 +++++---- src/components/Views/Topic.tsx | 28 ++-- src/context/following.tsx | 127 +++++++++++++++++ src/context/reactions.tsx | 2 +- src/context/session.tsx | 128 ++++++++--------- src/graphql/client/core.ts | 23 ++- src/graphql/mutation/core/reaction-create.ts | 1 - src/graphql/query/core/authors-load-by.ts | 2 +- .../query/core/communities-followed-by.ts | 17 +++ ...ics-by-author.ts => topics-followed-by.ts} | 2 +- src/pages/types.ts | 2 +- src/stores/zine/common.ts | 9 -- src/utils/getImageUrl.ts | 2 +- src/utils/useEscKeyDownHandler.ts | 4 +- 30 files changed, 536 insertions(+), 437 deletions(-) create mode 100644 src/context/following.tsx create mode 100644 src/graphql/query/core/communities-followed-by.ts rename src/graphql/query/core/{topics-by-author.ts => topics-followed-by.ts} (77%) delete mode 100644 src/stores/zine/common.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 0007cc26..bb2138ff 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -23,7 +23,7 @@ module.exports = { }, extends: [ 'plugin:@typescript-eslint/recommended', - // 'plugin:@typescript-eslint/recommended-requiring-type-checking', // 23-01-2024 681 problems + // 'plugin:@typescript-eslint/recommended-requiring-type-checking', // 30-01-2024 699 problems ], rules: { '@typescript-eslint/no-unused-vars': [ @@ -40,12 +40,12 @@ module.exports = { env: { browser: true, node: true, - mocha: true, + // mocha: true, }, globals: {}, rules: { // Solid - 'solid/reactivity': 'off', // too many 'should be used within JSX' + 'solid/reactivity': 'off', 'solid/no-innerhtml': 'off', /** Unicorn **/ @@ -65,6 +65,7 @@ module.exports = { 'unicorn/no-array-callback-reference': 'warn', 'unicorn/no-array-method-this-argument': 'warn', 'unicorn/no-for-loop': 'off', + 'unicorn/prefer-switch': 'warn', 'sonarjs/no-duplicate-string': ['warn', { threshold: 5 }], 'sonarjs/prefer-immediate-return': 'warn', diff --git a/.gitignore b/.gitignore index e36653bf..c1695636 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ stats.html *.scss.d.ts pnpm-lock.yaml bun.lockb +.jj diff --git a/package.json b/package.json index d52e5fc7..b1a646eb 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "typograf": "7.1.0", "uniqolor": "1.1.0", "vike": "0.4.148", - "vite": "4.5.1", + "vite": "4.5.2", "vite-plugin-mkcert": "1.16.0", "vite-plugin-sass-dts": "1.3.11", "vite-plugin-solid": "2.7.2", diff --git a/src/components/App.tsx b/src/components/App.tsx index 77a05db8..eae090f0 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -7,6 +7,7 @@ import { Dynamic } from 'solid-js/web' import { ConfirmProvider } from '../context/confirm' import { ConnectProvider } from '../context/connect' import { EditorProvider } from '../context/editor' +import { FollowingProvider } from '../context/following' import { InboxProvider } from '../context/inbox' import { LocalizeProvider } from '../context/localize' import { MediaQueryProvider } from '../context/mediaQuery' @@ -90,7 +91,7 @@ type Props = PageProps & { is404: boolean } export const App = (props: Props) => { const { page, searchParams } = useRouter() - let is404 = props.is404 + const is404 = createMemo(() => props.is404) createEffect(() => { if (!searchParams().m) { @@ -106,8 +107,7 @@ export const App = (props: Props) => { const pageComponent = createMemo(() => { const result = pagesMap[page()?.route || 'home'] - if (is404 || !result || page()?.path === '/404') { - is404 = false + if (is404() || !result || page()?.path === '/404') { return FourOuFourPage } @@ -122,15 +122,17 @@ export const App = (props: Props) => { - - - - - - - - - + + + + + + + + + + + diff --git a/src/components/Author/AuthorBadge/AuthorBadge.tsx b/src/components/Author/AuthorBadge/AuthorBadge.tsx index a596075a..2c90471b 100644 --- a/src/components/Author/AuthorBadge/AuthorBadge.tsx +++ b/src/components/Author/AuthorBadge/AuthorBadge.tsx @@ -2,13 +2,12 @@ import { openPage } from '@nanostores/router' import { clsx } from 'clsx' import { createEffect, createMemo, createSignal, Match, Show, Switch } from 'solid-js' +import { useFollowing } from '../../../context/following' import { useLocalize } from '../../../context/localize' import { useMediaQuery } from '../../../context/mediaQuery' import { useSession } from '../../../context/session' import { Author, FollowingEntity } from '../../../graphql/schema/core.gen' import { router, useRouter } from '../../../stores/router' -import { follow, unfollow } from '../../../stores/zine/common' -// import { capitalize } from '../../../utils/capitalize' import { isCyrillic } from '../../../utils/cyrillic' import { translit } from '../../../utils/ru2en' import { Button } from '../../_shared/Button' @@ -33,7 +32,7 @@ type Props = { export const AuthorBadge = (props: Props) => { const { mediaMatches } = useMediaQuery() const [isMobileView, setIsMobileView] = createSignal(false) - const [isSubscribing, setIsSubscribing] = createSignal(false) + const [followed, setFollowed] = createSignal(false) createEffect(() => { setIsMobileView(!mediaMatches.sm) @@ -41,33 +40,14 @@ export const AuthorBadge = (props: Props) => { const { author, - subscriptions, - actions: { loadSubscriptions, requireAuthentication }, + actions: { requireAuthentication }, } = useSession() + const { setFollowing } = useFollowing() const { changeSearchParams } = useRouter() const { t, formatDate, lang } = useLocalize() - const subscribed = createMemo(() => { - const sss = subscriptions() - return sss?.authors.some((a: Author) => a?.slug === props.author.slug) - }) - - const subscribe = async (really = true) => { - setIsSubscribing(true) - - await (really - ? follow({ what: FollowingEntity.Author, slug: props.author.slug }) - : unfollow({ what: FollowingEntity.Author, slug: props.author.slug })) - - await loadSubscriptions() - setIsSubscribing(false) - } - const handleSubscribe = (really: boolean) => { - requireAuthentication(() => { - subscribe(really) - }, 'subscribe') - } const initChat = () => { + // eslint-disable-next-line solid/reactivity requireAuthentication(() => { openPage(router, `inbox`) changeSearchParams({ @@ -88,6 +68,14 @@ export const AuthorBadge = (props: Props) => { return props.author.name }) + const handleFollowClick = () => { + const value = !followed() + requireAuthentication(() => { + setFollowed(value) + setFollowing(FollowingEntity.Author, props.author.slug, value) + }, 'subscribe') + } + return (
@@ -135,37 +123,24 @@ export const AuthorBadge = (props: Props) => {
handleSubscribe(!subscribed())} - /> - } + fallback={} > - {t('subscribing...')} - - } - > + } - onClick={() => handleSubscribe(true)} + onClick={handleFollowClick} isSubscribeButton={true} class={clsx(styles.actionButton, { [styles.iconed]: props.iconButtons, - [stylesButton.subscribed]: subscribed(), + [stylesButton.subscribed]: followed(), })} /> } @@ -186,11 +161,11 @@ export const AuthorBadge = (props: Props) => { } - onClick={() => handleSubscribe(false)} + onClick={handleFollowClick} isSubscribeButton={true} class={clsx(styles.actionButton, { [styles.iconed]: props.iconButtons, - [stylesButton.subscribed]: subscribed(), + [stylesButton.subscribed]: followed(), })} /> diff --git a/src/components/Author/AuthorCard/AuthorCard.tsx b/src/components/Author/AuthorCard/AuthorCard.tsx index 587dd564..336d908f 100644 --- a/src/components/Author/AuthorCard/AuthorCard.tsx +++ b/src/components/Author/AuthorCard/AuthorCard.tsx @@ -1,15 +1,15 @@ -import type { Author } from '../../../graphql/schema/core.gen' +import type { Author, Community } from '../../../graphql/schema/core.gen' import { openPage, redirectPage } from '@nanostores/router' import { clsx } from 'clsx' -import { createEffect, createMemo, createSignal, For, Show } from 'solid-js' +import { createEffect, createMemo, createSignal, For, onMount, Show } from 'solid-js' +import { useFollowing } from '../../../context/following' import { useLocalize } from '../../../context/localize' import { useSession } from '../../../context/session' import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen' import { SubscriptionFilter } from '../../../pages/types' import { router, useRouter } from '../../../stores/router' -import { follow, unfollow } from '../../../stores/zine/common' import { isCyrillic } from '../../../utils/cyrillic' import { isAuthor } from '../../../utils/isAuthor' import { translit } from '../../../utils/ru2en' @@ -33,32 +33,14 @@ export const AuthorCard = (props: Props) => { const { t, lang } = useLocalize() const { author, - subscriptions, isSessionLoaded, - actions: { loadSubscriptions, requireAuthentication }, + actions: { requireAuthentication }, } = useSession() - - const [isSubscribing, setIsSubscribing] = createSignal(false) - const [following, setFollowing] = createSignal>(props.following) + const [authorSubs, setAuthorSubs] = createSignal>([]) const [subscriptionFilter, setSubscriptionFilter] = createSignal('all') - - const subscribed = createMemo(() => - subscriptions().authors.some((a: Author) => a?.slug === props.author.slug), - ) - - const subscribe = async (really = true) => { - setIsSubscribing(true) - - await (really - ? follow({ what: FollowingEntity.Author, slug: props.author.slug }) - : unfollow({ what: FollowingEntity.Author, slug: props.author.slug })) - - await loadSubscriptions() - setIsSubscribing(false) - } - const isProfileOwner = createMemo(() => author()?.slug === props.author.slug) - + const [followed, setFollowed] = createSignal() + const { setFollowing } = useFollowing() const name = createMemo(() => { if (lang() !== 'ru' && isCyrillic(props.author.name)) { if (props.author.name === 'Дискурс') { @@ -71,9 +53,12 @@ export const AuthorCard = (props: Props) => { return props.author.name }) + onMount(() => setAuthorSubs(props.following)) + // TODO: reimplement AuthorCard const { changeSearchParams } = useRouter() const initChat = () => { + // eslint-disable-next-line solid/reactivity requireAuthentication(() => { openPage(router, `inbox`) changeSearchParams({ @@ -82,30 +67,30 @@ export const AuthorCard = (props: Props) => { }, 'discussions') } - const handleSubscribe = () => { - requireAuthentication(() => { - subscribe(!subscribed()) - }, 'subscribe') - } - createEffect(() => { if (props.following) { - if (subscriptionFilter() === 'users') { - setFollowing(props.following.filter((s) => 'name' in s)) + if (subscriptionFilter() === 'authors') { + setAuthorSubs(props.following.filter((s) => 'name' in s)) } else if (subscriptionFilter() === 'topics') { - setFollowing(props.following.filter((s) => 'title' in s)) + setAuthorSubs(props.following.filter((s) => 'title' in s)) + } else if (subscriptionFilter() === 'communities') { + setAuthorSubs(props.following.filter((s) => 'title' in s)) } else { - setFollowing(props.following) + setAuthorSubs(props.following) } } }) - const followButtonText = createMemo(() => { - if (isSubscribing()) { - return t('subscribing...') - } + const handleFollowClick = () => { + const value = !followed() + requireAuthentication(() => { + setFollowed(value) + setFollowing(FollowingEntity.Author, props.author.slug, value) + }, 'subscribe') + } - if (subscribed()) { + const followButtonText = createMemo(() => { + if (followed()) { return ( <> {t('Following')} @@ -214,11 +199,11 @@ export const AuthorCard = (props: Props) => { fallback={
{props.following.length} -
  • - @@ -300,7 +285,7 @@ export const AuthorCard = (props: Props) => {
    - + {(subscription) => isAuthor(subscription) ? ( diff --git a/src/components/Feed/Row2.tsx b/src/components/Feed/Row2.tsx index 73c5fa70..5fa5a6be 100644 --- a/src/components/Feed/Row2.tsx +++ b/src/components/Feed/Row2.tsx @@ -1,15 +1,10 @@ import type { Shout } from '../../graphql/schema/core.gen' -import { createComputed, createSignal, Show, For } from 'solid-js' +import { createSignal, createEffect, For, Show } from 'solid-js' import { ArticleCard } from './ArticleCard' -import { ArticleCardProps } from './ArticleCard/ArticleCard' -const x = [ - ['12', '12'], - ['8', '16'], - ['16', '8'], -] +const columnSizes = ['col-md-12', 'col-md-8', 'col-md-16'] export const Row2 = (props: { articles: Shout[] @@ -18,10 +13,10 @@ export const Row2 = (props: { noAuthorLink?: boolean noauthor?: boolean }) => { - const [y, setY] = createSignal(0) + const [columnIndex, setColumnIndex] = createSignal(0) - // FIXME: random can break hydration - createComputed(() => setY(Math.floor(Math.random() * x.length))) + // Update column index on component mount + createEffect(() => setColumnIndex(Math.floor(Math.random() * columnSizes.length))) return ( 0}> @@ -29,31 +24,16 @@ export const Row2 = (props: {
    - {(a, i) => { - // FIXME: refactor this, too ugly now - const className = `col-md-${props.isEqual ? '12' : x[y()][i()]}` - let desktopCoverSize: ArticleCardProps['desktopCoverSize'] - - switch (className) { - case 'col-md-8': { - desktopCoverSize = 'S' - break - } - case 'col-md-12': { - desktopCoverSize = 'M' - break - } - default: { - desktopCoverSize = 'L' - } - } - + {(article, _idx) => { + const className = columnSizes[props.isEqual ? 0 : columnIndex() % columnSizes.length] + const big = className === 'col-md-12' ? 'M' : 'L' + const desktopCoverSize = className === 'col-md-8' ? 'S' : big return (
    { const { t } = useLocalize() const { seen } = useSeenStore() - const { subscriptions } = useSession() + const { subscriptions } = useFollowing() const { page } = useRouter() const { articlesByTopic } = useArticlesStore() const [isSubscriptionsVisible, setSubscriptionsVisible] = createSignal(true) @@ -27,7 +28,6 @@ export const Sidebar = () => { const checkAuthorIsSeen = (authorSlug: string) => { return Boolean(seen()[authorSlug]) } - return (
      @@ -111,7 +111,7 @@ export const Sidebar = () => {
    - 0 || subscriptions().topics.length > 0}> + 0 || subscriptions.topics.length > 0}>

    { @@ -122,22 +122,19 @@ export const Sidebar = () => {

      - - {(author) => ( + + {(a: Author) => (
    • - +
      - -
      {author.name}
      + +
      {a.name}
    • )}
      - + {(topic) => (
    • { } onMount(async () => { - const topics = await apiClient.getRandomTopics({ amount: RANDOM_TOPICS_COUNT }) - setRandomTopics(topics) + if (window.location.pathname === '/' || window.location.pathname === '') { + const topics = await apiClient.getRandomTopics({ amount: RANDOM_TOPICS_COUNT }) + setRandomTopics(topics) + } }) const handleToggleMenuByLink = (event: MouseEvent, route: keyof typeof ROUTES) => { diff --git a/src/components/ProfileSettings/ProfileSettings.tsx b/src/components/ProfileSettings/ProfileSettings.tsx index 490c289a..29382f3b 100644 --- a/src/components/ProfileSettings/ProfileSettings.tsx +++ b/src/components/ProfileSettings/ProfileSettings.tsx @@ -38,7 +38,7 @@ export const ProfileSettings = () => { const [addLinkForm, setAddLinkForm] = createSignal(false) const [incorrectUrl, setIncorrectUrl] = createSignal(false) const [isUserpicUpdating, setIsUserpicUpdating] = createSignal(false) - const [userpicFile, setUserpicFile] = createSignal(null) + const [userpicFile, setUserpicFile] = createSignal(null) const [uploadError, setUploadError] = createSignal(false) const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false) const [hostname, setHostname] = createSignal(null) diff --git a/src/components/Topic/Card.tsx b/src/components/Topic/Card.tsx index 23dd980e..917aa396 100644 --- a/src/components/Topic/Card.tsx +++ b/src/components/Topic/Card.tsx @@ -1,12 +1,10 @@ -import type { Topic } from '../../graphql/schema/core.gen' - import { clsx } from 'clsx' import { createMemo, createSignal, Show } from 'solid-js' +import { useFollowing } from '../../context/following' import { useLocalize } from '../../context/localize' import { useSession } from '../../context/session' -import { FollowingEntity } from '../../graphql/schema/core.gen' -import { follow, unfollow } from '../../stores/zine/common' +import { FollowingEntity, type Topic } from '../../graphql/schema/core.gen' import { capitalize } from '../../utils/capitalize' import { Button } from '../_shared/Button' import { CheckButton } from '../_shared/CheckButton' @@ -36,32 +34,21 @@ interface TopicProps { export const TopicCard = (props: TopicProps) => { const { t, lang } = useLocalize() + const title = createMemo(() => + capitalize(lang() === 'en' ? props.topic.slug.replaceAll('-', ' ') : props.topic.title || ''), + ) const { - subscriptions, - isSessionLoaded, - actions: { loadSubscriptions, requireAuthentication }, + author, + actions: { requireAuthentication }, } = useSession() + const { setFollowing, loading: subLoading } = useFollowing() + const [followed, setFollowed] = createSignal() - const [isSubscribing, setIsSubscribing] = createSignal(false) - - const subscribed = createMemo(() => { - return subscriptions().topics.some((topic) => topic.slug === props.topic.slug) - }) - - const subscribe = async (really = true) => { - setIsSubscribing(true) - - await (really - ? follow({ what: FollowingEntity.Topic, slug: props.topic.slug }) - : unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug })) - - await loadSubscriptions() - setIsSubscribing(false) - } - - const handleSubscribe = () => { + const handleFollowClick = () => { + const value = !followed() requireAuthentication(() => { - subscribe(!subscribed()) + setFollowed(value) + setFollowing(FollowingEntity.Topic, props.topic.slug, value) }, 'subscribe') } @@ -69,12 +56,12 @@ export const TopicCard = (props: TopicProps) => { return ( <> - + - + {t('Unfollow')} {t('Following')} @@ -83,10 +70,6 @@ export const TopicCard = (props: TopicProps) => { ) } - const title = createMemo(() => - capitalize(lang() === 'en' ? props.topic.slug.replaceAll('-', ' ') : props.topic.title || ''), - ) - return (
      { }} > - + + } >
    • -
    • -
    • diff --git a/src/components/Views/PublishSettings/PublishSettings.tsx b/src/components/Views/PublishSettings/PublishSettings.tsx index b5a144e4..1fe168be 100644 --- a/src/components/Views/PublishSettings/PublishSettings.tsx +++ b/src/components/Views/PublishSettings/PublishSettings.tsx @@ -1,6 +1,6 @@ import { redirectPage } from '@nanostores/router' import { clsx } from 'clsx' -import { createEffect, createSignal, lazy, onMount, Show } from 'solid-js' +import { createEffect, createMemo, createSignal, lazy, onMount, Show } from 'solid-js' import { createStore } from 'solid-js/store' import { ShoutForm, useEditorContext } from '../../../context/editor' @@ -16,7 +16,6 @@ import { Icon } from '../../_shared/Icon' import { Image } from '../../_shared/Image' import { TopicSelect, UploadModalContent } from '../../Editor' import { Modal } from '../../Nav/Modal' -import { EMPTY_TOPIC } from '../Edit' import styles from './PublishSettings.module.scss' import stylesBeside from '../../Feed/Beside.module.scss' @@ -36,21 +35,26 @@ const shorten = (str: string, maxLen: number) => { return `${result}...` } +const EMPTY_TOPIC: Topic = { + id: -1, + slug: '', +} +const emptyConfig = { + coverImageUrl: '', + mainTopic: EMPTY_TOPIC, + slug: '', + title: '', + subtitle: '', + description: '', + selectedTopics: [], +} + export const PublishSettings = (props: Props) => { const { t } = useLocalize() const { author } = useSession() const { sortedTopics } = useTopicsStore() - const [topics, setTopics] = createSignal(sortedTopics()) - onMount(async () => { - await loadAllTopics() - }) - - createEffect(() => { - setTopics(sortedTopics()) - }) - const composeDescription = () => { if (!props.form.description) { const cleanFootnotes = props.form.body.replaceAll(/.*?<\/footnote>/g, '') @@ -60,23 +64,32 @@ export const PublishSettings = (props: Props) => { return props.form.description } - const initialData: Partial = { - coverImageUrl: props.form.coverImageUrl, - mainTopic: props.form.mainTopic || EMPTY_TOPIC, - slug: props.form.slug, - title: props.form.title, - subtitle: props.form.subtitle, - description: composeDescription(), - selectedTopics: [], - } + const initialData = createMemo(() => { + return { + coverImageUrl: props.form?.coverImageUrl, + mainTopic: props.form?.mainTopic || EMPTY_TOPIC, + slug: props.form?.slug, + title: props.form?.title, + subtitle: props.form?.subtitle, + description: composeDescription(), + selectedTopics: [], + } + }) + + const [settingsForm, setSettingsForm] = createStore(emptyConfig) + + onMount(() => { + setSettingsForm(initialData()) + loadAllTopics() + }) + + createEffect(() => setTopics(sortedTopics())) const { formErrors, actions: { setForm, setFormErrors, saveShout, publishShout }, } = useEditorContext() - const [settingsForm, setSettingsForm] = createStore(initialData) - const handleUploadModalContentCloseSetCover = (image: UploadedFile) => { hideModal() setSettingsForm('coverImageUrl', image.url) @@ -110,7 +123,7 @@ export const PublishSettings = (props: Props) => { }) } const handleCancelClick = () => { - setSettingsForm(initialData) + setSettingsForm(initialData()) handleBackClick() } const handlePublishSubmit = () => { @@ -149,9 +162,9 @@ export const PublishSettings = (props: Props) => { [styles.hasImage]: settingsForm.coverImageUrl, })} > - +
      - {initialData.title} + {initialData().title}
      diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx index a6702147..1b3d059a 100644 --- a/src/components/Views/Topic.tsx +++ b/src/components/Views/Topic.tsx @@ -2,7 +2,7 @@ import type { Shout, Topic } from '../../graphql/schema/core.gen' import { Meta } from '@solidjs/meta' import { clsx } from 'clsx' -import { For, Show, createMemo, onMount, createSignal } from 'solid-js' +import { For, Show, createMemo, onMount, createSignal, createEffect } from 'solid-js' import { useLocalize } from '../../context/localize' import { useRouter } from '../../stores/router' @@ -39,23 +39,27 @@ const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3 export const TopicView = (props: Props) => { const { t, lang } = useLocalize() const { searchParams, changeSearchParams } = useRouter() - const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) - const { sortedArticles } = useArticlesStore({ shouts: props.shouts }) const { topicEntities } = useTopicsStore({ topics: [props.topic] }) - const { authorsByTopic } = useAuthorsStore() - const topic = createMemo(() => - props.topic?.slug in topicEntities() ? topicEntities()[props.topic.slug] : props.topic, + const [topic, setTopic] = createSignal() + createEffect(() => { + const topics = topicEntities() + if (props.topicSlug && !topic() && topics) { + setTopic(topics[props.topicSlug]) + } + }) + const title = createMemo( + () => + `#${capitalize( + lang() === 'en' + ? topic()?.slug.replace(/-/, ' ') + : topic()?.title || topic()?.slug.replace(/-/, ' '), + true, + )}`, ) - const title = () => - `#${capitalize( - lang() === 'en' ? topic()?.slug.replace(/-/, ' ') : topic()?.title || topic()?.slug.replace(/-/, ' '), - true, - )}` - onMount(() => (document.title = title())) const loadMore = async () => { saveScrollPosition() diff --git a/src/context/following.tsx b/src/context/following.tsx new file mode 100644 index 00000000..2246485f --- /dev/null +++ b/src/context/following.tsx @@ -0,0 +1,127 @@ +import { createEffect, createSignal, createContext, Accessor, useContext, JSX, onMount } from 'solid-js' +import { createStore } from 'solid-js/store' + +import { apiClient } from '../graphql/client/core' +import { Author, Community, FollowingEntity, Topic } from '../graphql/schema/core.gen' + +import { useSession } from './session' + +type SubscriptionsData = { + topics?: Topic[] + authors?: Author[] + communities?: Community[] +} + +interface FollowingContextType { + loading: Accessor + subscriptions: SubscriptionsData + setSubscriptions: (subscriptions: SubscriptionsData) => void + setFollowing: (what: FollowingEntity, slug: string, value: boolean) => void + loadSubscriptions: () => void + follow: (what: FollowingEntity, slug: string) => Promise + unfollow: (what: FollowingEntity, slug: string) => Promise +} + +const FollowingContext = createContext() + +export function useFollowing() { + return useContext(FollowingContext) +} + +const EMPTY_SUBSCRIPTIONS = { + topics: [], + authors: [], + communities: [], +} + +export const FollowingProvider = (props: { children: JSX.Element }) => { + const [loading, setLoading] = createSignal(false) + const [subscriptions, setSubscriptions] = createStore(EMPTY_SUBSCRIPTIONS) + const { author } = useSession() + + const fetchData = async () => { + setLoading(true) + try { + if (apiClient.private) { + console.debug('[context.following] fetching subs data...') + const result = await apiClient.getMySubscriptions() + setSubscriptions(result || EMPTY_SUBSCRIPTIONS) + console.info('[context.following] subs:', subscriptions) + } + } catch (error) { + console.info('[context.following] cannot get subs', error) + } finally { + setLoading(false) + } + } + + const follow = async (what: FollowingEntity, slug: string) => { + if (!author()) return + try { + await apiClient.follow({ what, slug }) + setSubscriptions((prevSubscriptions) => { + const updatedSubs = { ...prevSubscriptions } + if (!updatedSubs[what]) updatedSubs[what] = [] + const exists = updatedSubs[what]?.some((entity) => entity.slug === slug) + if (!exists) updatedSubs[what].push(slug) + return updatedSubs + }) + } catch (error) { + console.error(error) + } + } + + const unfollow = async (what: FollowingEntity, slug: string) => { + if (!author()) return + try { + await apiClient.unfollow({ what, slug }) + } catch (error) { + console.error(error) + } + } + + const loadData = (_a?: Author) => { + // console.debug('[context.following] current subs:', subscriptions) + if (!subscriptions?.authors?.length && !subscriptions?.topics?.length) { + // && subscriptions.communites?.length + fetchData() + } + } + createEffect(() => { + if (author()) loadData() + }) + onMount(loadData) + + const setFollowing = (what: FollowingEntity, slug: string, value = true) => { + setSubscriptions((prevSubscriptions) => { + const updatedSubs = { ...prevSubscriptions } + if (!updatedSubs[what]) updatedSubs[what] = [] + if (value) { + const exists = updatedSubs[what]?.some((entity) => entity.slug === slug) + if (!exists) updatedSubs[what].push(slug) + } else { + updatedSubs[what] = (updatedSubs[what] || []).filter((x) => x !== slug) + } + return updatedSubs + }) + try { + ;(value ? follow : unfollow)(what, slug) + } catch (error) { + console.error(error) + } finally { + fetchData() + } + } + + const value: FollowingContextType = { + loading, + subscriptions, + setSubscriptions, + setFollowing, + loadSubscriptions: fetchData, + follow, + unfollow, + } + + return {props.children} +} diff --git a/src/context/reactions.tsx b/src/context/reactions.tsx index c98ebcaa..5af22fe2 100644 --- a/src/context/reactions.tsx +++ b/src/context/reactions.tsx @@ -56,7 +56,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { const createReaction = async (input: ReactionInput): Promise => { const reaction = await apiClient.createReaction(input) - + if (!reaction) return const changes = { [reaction.id]: reaction, } diff --git a/src/context/session.tsx b/src/context/session.tsx index 0b872ab4..d637edb9 100644 --- a/src/context/session.tsx +++ b/src/context/session.tsx @@ -1,5 +1,5 @@ import type { AuthModalSource } from '../components/Nav/AuthModal/types' -import type { Author, Result } from '../graphql/schema/core.gen' +import type { Author } from '../graphql/schema/core.gen' import type { Accessor, JSX, Resource } from 'solid-js' import { @@ -41,19 +41,17 @@ const defaultConfig: ConfigType = { } export type SessionContextType = { - config: ConfigType + config: Accessor session: Resource author: Resource authError: Accessor isSessionLoaded: Accessor - subscriptions: Accessor isAuthenticated: Accessor actions: { loadSession: () => AuthToken | Promise setSession: (token: AuthToken | null) => void // setSession loadAuthor: (info?: unknown) => Author | Promise setAuthor: (a: Author) => void - loadSubscriptions: () => Promise requireAuthentication: ( callback: (() => Promise) | (() => void), modalSource: AuthModalSource, @@ -77,11 +75,6 @@ export function useSession() { return useContext(SessionContext) } -const EMPTY_SUBSCRIPTIONS = { - topics: [], - authors: [], -} - export const SessionProvider = (props: { onStateChangeCallback(state: AuthToken): unknown children: JSX.Element @@ -91,12 +84,12 @@ export const SessionProvider = (props: { actions: { showSnackbar }, } = useSnackbar() const { searchParams, changeSearchParams } = useRouter() - const [configuration, setConfig] = createSignal(defaultConfig) - const authorizer = createMemo(() => new Authorizer(configuration())) + const [config, setConfig] = createSignal(defaultConfig) + const authorizer = createMemo(() => new Authorizer(config())) const [oauthState, setOauthState] = createSignal() // handle callback's redirect_uri - createEffect(async () => { + createEffect(() => { // oauth const state = searchParams()?.state if (state) { @@ -120,43 +113,44 @@ export const SessionProvider = (props: { }) // load - let minuteLater + let minuteLater: NodeJS.Timeout | null const [isSessionLoaded, setIsSessionLoaded] = createSignal(false) const [authError, setAuthError] = createSignal('') - const [session, { refetch: loadSession, mutate: setSession }] = createResource( - async () => { - try { - const s = await authorizer().getSession() - console.info('[context.session] loading session', s) - // Set session expiration time in local storage - const expires_at = new Date(Date.now() + s.expires_in * 1000) - localStorage.setItem('expires_at', `${expires_at.getTime()}`) + // Function to load session data + const sessionData = async () => { + try { + const s = await authorizer().getSession() + console.info('[context.session] loading session', s) - // Set up session expiration check timer - minuteLater = setTimeout(checkSessionIsExpired, 60 * 1000) - console.info(`[context.session] will refresh in ${s.expires_in / 60} mins`) + // Set session expiration time in local storage + const expires_at = new Date(Date.now() + s.expires_in * 1000) + localStorage.setItem('expires_at', `${expires_at.getTime()}`) - // Set the session loaded flag - setIsSessionLoaded(true) + // Set up session expiration check timer + minuteLater = setTimeout(checkSessionIsExpired, 60 * 1000) + console.info(`[context.session] will refresh in ${s.expires_in / 60} mins`) - return s - } catch (error) { - console.info('[context.session] cannot refresh session', error) - setAuthError(error) + // Set the session loaded flag + setIsSessionLoaded(true) - // Set the session loaded flag even if there's an error - setIsSessionLoaded(true) + return s + } catch (error) { + console.info('[context.session] cannot refresh session', error) + setAuthError(error) - return null - } - }, - { - ssrLoadFrom: 'initial', - initialValue: null, - }, - ) + // Set the session loaded flag even if there's an error + setIsSessionLoaded(true) + + return null + } + } + + const [session, { refetch: loadSession, mutate: setSession }] = createResource(sessionData, { + ssrLoadFrom: 'initial', + initialValue: null, + }) const checkSessionIsExpired = () => { const expires_at_data = localStorage.getItem('expires_at') @@ -177,26 +171,17 @@ export const SessionProvider = (props: { } onCleanup(() => clearTimeout(minuteLater)) - - const [author, { refetch: loadAuthor, mutate: setAuthor }] = createResource( - async () => { - const u = session()?.user - return u ? (await apiClient.getAuthorId({ user: u.id.trim() })) || null : null - }, - { - ssrLoadFrom: 'initial', - initialValue: null, - }, - ) - - const [subscriptions, setSubscriptions] = createSignal(EMPTY_SUBSCRIPTIONS) - const loadSubscriptions = async (): Promise => { - const result = await apiClient.getMySubscriptions() - setSubscriptions(result || EMPTY_SUBSCRIPTIONS) + const authorData = async () => { + const u = session()?.user + return u ? (await apiClient.getAuthorId({ user: u.id.trim() })) || null : null } + const [author, { refetch: loadAuthor, mutate: setAuthor }] = createResource(authorData, { + ssrLoadFrom: 'initial', + initialValue: null, + }) // when session is loaded - createEffect(async () => { + createEffect(() => { if (session()) { const token = session()?.access_token if (token) { @@ -206,23 +191,24 @@ export const SessionProvider = (props: { notifierClient.connect(token) inboxClient.connect(token) } - if (!author()) { - const a = await loadAuthor() - if (a) { - await loadSubscriptions() - addAuthors([a]) - } else { - reset() - } - } + if (!author()) loadAuthor() + setIsSessionLoaded(true) } } }) + // when author is loaded + createEffect(() => { + if (author()) { + addAuthors([author()]) + } else { + reset() + } + }) + const reset = () => { setIsSessionLoaded(true) - setSubscriptions(EMPTY_SUBSCRIPTIONS) setSession(null) setAuthor(null) } @@ -252,10 +238,10 @@ export const SessionProvider = (props: { ) const [authCallback, setAuthCallback] = createSignal<() => void>(() => {}) - const requireAuthentication = async (callback: () => void, modalSource: AuthModalSource) => { + const requireAuthentication = (callback: () => void, modalSource: AuthModalSource) => { setAuthCallback((_cb) => callback) if (!session()) { - await loadSession() + loadSession() if (!session()) { showModal('auth', modalSource) } @@ -323,7 +309,6 @@ export const SessionProvider = (props: { const isAuthenticated = createMemo(() => Boolean(author())) const actions = { loadSession, - loadSubscriptions, requireAuthentication, signUp, signIn, @@ -339,9 +324,8 @@ export const SessionProvider = (props: { } const value: SessionContextType = { authError, - config: configuration(), + config, session, - subscriptions, isSessionLoaded, author, actions, diff --git a/src/graphql/client/core.ts b/src/graphql/client/core.ts index 38ed9705..04e8acf6 100644 --- a/src/graphql/client/core.ts +++ b/src/graphql/client/core.ts @@ -12,6 +12,7 @@ import type { QueryLoad_Authors_ByArgs, QueryLoad_Shouts_SearchArgs, QueryLoad_Shouts_Random_TopArgs, + Community, } from '../schema/core.gen' import { createGraphQLClient } from '../createGraphQLClient' @@ -37,20 +38,24 @@ import authorBy from '../query/core/author-by' import authorFollowers from '../query/core/author-followers' import authorId from '../query/core/author-id' import authorsAll from '../query/core/authors-all' -import authorFollowed from '../query/core/authors-followed-by' import authorsLoadBy from '../query/core/authors-load-by' import mySubscriptions from '../query/core/my-followed' import reactionsLoadBy from '../query/core/reactions-load-by' import topicBySlug from '../query/core/topic-by-slug' import topicsAll from '../query/core/topics-all' -import userFollowedTopics from '../query/core/topics-by-author' +import authorFollowedAuthors from '../query/core/authors-followed-by' +import authorFollowedTopics from '../query/core/topics-followed-by' +import authorFollowedCommunities from '../query/core/communities-followed-by' import topicsRandomQuery from '../query/core/topics-random' const publicGraphQLClient = createGraphQLClient('core') export const apiClient = { private: null, - connect: (token: string) => (apiClient.private = createGraphQLClient('core', token)), // NOTE: use it after token appears + connect: (token: string) => { + // NOTE: use it after token appears + apiClient.private = createGraphQLClient('core', token) + }, getRandomTopShouts: async (params: QueryLoad_Shouts_Random_TopArgs) => { const response = await publicGraphQLClient.query(loadShoutsTopRandom, params).toPromise() @@ -119,14 +124,18 @@ export const apiClient = { const response = await publicGraphQLClient.query(authorFollowers, { slug }).toPromise() return response.data.get_author_followers }, - getAuthorFollowingUsers: async ({ slug }: { slug: string }): Promise => { - const response = await publicGraphQLClient.query(authorFollowed, { slug }).toPromise() + getAuthorFollowingAuthors: async ({ slug }: { slug: string }): Promise => { + const response = await publicGraphQLClient.query(authorFollowedAuthors, { slug }).toPromise() return response.data.get_author_followed }, getAuthorFollowingTopics: async ({ slug }: { slug: string }): Promise => { - const response = await publicGraphQLClient.query(userFollowedTopics, { slug }).toPromise() + const response = await publicGraphQLClient.query(authorFollowedTopics, { slug }).toPromise() return response.data.get_topics_by_author }, + getAuthorFollowingCommunities: async ({ slug }: { slug: string }): Promise => { + const response = await publicGraphQLClient.query(authorFollowedCommunities, { slug }).toPromise() + return response.data.get_communities_by_author + }, updateProfile: async (input: ProfileInput) => { const response = await apiClient.private.mutation(updateProfile, { profile: input }).toPromise() return response.data.update_profile @@ -200,7 +209,7 @@ export const apiClient = { const resp = await publicGraphQLClient.query(shoutsLoadBy, { options }).toPromise() if (resp.error) console.error(resp) - return resp.data.load_shouts_by + return resp.data?.load_shouts_by }, getShoutsSearch: async ({ text, limit, offset }: QueryLoad_Shouts_SearchArgs) => { diff --git a/src/graphql/mutation/core/reaction-create.ts b/src/graphql/mutation/core/reaction-create.ts index c85d3353..72852d97 100644 --- a/src/graphql/mutation/core/reaction-create.ts +++ b/src/graphql/mutation/core/reaction-create.ts @@ -8,7 +8,6 @@ export default gql` id body kind - range created_at reply_to stat { diff --git a/src/graphql/query/core/authors-load-by.ts b/src/graphql/query/core/authors-load-by.ts index 11e11be2..427889c4 100644 --- a/src/graphql/query/core/authors-load-by.ts +++ b/src/graphql/query/core/authors-load-by.ts @@ -1,7 +1,7 @@ import { gql } from '@urql/core' export default gql` - query AuthorsAllQuery($by: AuthorsBy, $limit: Int, $offset: Int) { + query AuthorsAllQuery($by: AuthorsBy!, $limit: Int, $offset: Int) { load_authors_by(by: $by, limit: $limit, offset: $offset) { id slug diff --git a/src/graphql/query/core/communities-followed-by.ts b/src/graphql/query/core/communities-followed-by.ts new file mode 100644 index 00000000..d5d33128 --- /dev/null +++ b/src/graphql/query/core/communities-followed-by.ts @@ -0,0 +1,17 @@ +import { gql } from '@urql/core' + +export default gql` + query LoadCommunitiesFollowedBy($slug: String, $user: String, $author_id: Int) { + get_communities_by_author(slug: $slug, user: $user, author_id: $author_id) { + id + slug + title + pic + stat { + shouts + followers + authors + } + } + } +` diff --git a/src/graphql/query/core/topics-by-author.ts b/src/graphql/query/core/topics-followed-by.ts similarity index 77% rename from src/graphql/query/core/topics-by-author.ts rename to src/graphql/query/core/topics-followed-by.ts index b5d7db23..b5299177 100644 --- a/src/graphql/query/core/topics-by-author.ts +++ b/src/graphql/query/core/topics-followed-by.ts @@ -1,7 +1,7 @@ import { gql } from '@urql/core' export default gql` - query UserFollowingTopicsQuery($slug: String, $user: String, $author_id: Int) { + query LoadTopicsFollowedBy($slug: String, $user: String, $author_id: Int) { get_topics_by_author(slug: $slug, user: $user, author_id: $author_id) { id slug diff --git a/src/pages/types.ts b/src/pages/types.ts index c54208c1..b36a7612 100644 --- a/src/pages/types.ts +++ b/src/pages/types.ts @@ -50,4 +50,4 @@ export type UploadedFile = { originalFilename?: string } -export type SubscriptionFilter = 'all' | 'users' | 'topics' +export type SubscriptionFilter = 'all' | 'authors' | 'topics' | 'communities' diff --git a/src/stores/zine/common.ts b/src/stores/zine/common.ts deleted file mode 100644 index c874816b..00000000 --- a/src/stores/zine/common.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { apiClient } from '../../graphql/client/core' -import { FollowingEntity } from '../../graphql/schema/core.gen' - -export const follow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => { - await apiClient.follow({ what, slug }) -} -export const unfollow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => { - await apiClient.unfollow({ what, slug }) -} diff --git a/src/utils/getImageUrl.ts b/src/utils/getImageUrl.ts index ef59e6d8..6f249115 100644 --- a/src/utils/getImageUrl.ts +++ b/src/utils/getImageUrl.ts @@ -15,7 +15,7 @@ export const getImageUrl = ( src: string, options: { width?: number; height?: number; noSizeUrlPart?: boolean } = {}, ) => { - const filename = src.split('/').pop() + const filename = src?.split('/').pop() const isAudio = src.toLowerCase().split('.').pop() in ['wav', 'mp3', 'ogg', 'aif', 'flac'] const base = isAudio ? cdnUrl : `${thumborUrl}/unsafe/` const sizeUrlPart = isAudio ? '' : getSizeUrlPart(options) diff --git a/src/utils/useEscKeyDownHandler.ts b/src/utils/useEscKeyDownHandler.ts index 3e19f505..b3430c31 100644 --- a/src/utils/useEscKeyDownHandler.ts +++ b/src/utils/useEscKeyDownHandler.ts @@ -1,9 +1,9 @@ import { onCleanup, onMount } from 'solid-js' -export const useEscKeyDownHandler = (onEscKeyDown: () => void) => { +export const useEscKeyDownHandler = (onEscKeyDown: (ev) => void) => { const keydownHandler = (e: KeyboardEvent) => { if (e.key === 'Escape') { - onEscKeyDown() + onEscKeyDown(e) } } From 0b443804bd4cafead7354e6a7154df3f4583efed Mon Sep 17 00:00:00 2001 From: Untone Date: Wed, 31 Jan 2024 15:55:18 +0300 Subject: [PATCH 02/14] authorizer-upgrade --- package-lock.json | 14 ++++---------- package.json | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc331cb5..5fb127b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,10 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@authorizerdev/authorizer-js": "1.2.11", + "@authorizerdev/authorizer-js": "2.0.0", "@solid-primitives/pagination": "0.2.10", "cropperjs": "1.6.1", "form-data": "4.0.0", - "ga-gtag": "1.2.0", "i18next": "22.4.15", "i18next-icu": "2.3.0", "idb": "7.1.1", @@ -387,9 +386,9 @@ } }, "node_modules/@authorizerdev/authorizer-js": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.2.11.tgz", - "integrity": "sha512-onATswFYM0QCmhFPJmjS+S7Z0GNqlekqkDdFK6Bj3OeMBDQufARRHmVIGVI+0IlB7TWW38D1l6WbTZin0ct+aA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-2.0.0.tgz", + "integrity": "sha512-PTVuCrCkZkVPoo+l0+9PVFyP9frLp/L3FUtQDtAaN+ERuqx97DNF20tIH8khSvnXrkKv3lTJ/5iFWddy+dTAwg==", "dependencies": { "cross-fetch": "^3.1.5" }, @@ -9532,11 +9531,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ga-gtag": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ga-gtag/-/ga-gtag-1.2.0.tgz", - "integrity": "sha512-j9gxutMdpGMdwaX1SzOG31Ddm+IGFjeNf+N3Z5g+BBpS8FSXOALlrM+ORIGc/QKszGJEDlw+6PfIsJZICsqsGQ==" - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", diff --git a/package.json b/package.json index 1bce74a2..006bd345 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "typecheck:watch": "tsc --noEmit --watch" }, "dependencies": { - "@authorizerdev/authorizer-js": "1.2.11", + "@authorizerdev/authorizer-js": "2.0.0", "@solid-primitives/pagination": "0.2.10", "cropperjs": "1.6.1", "form-data": "4.0.0", From 103e73a870fa1f706b21036d74342fcd6fc6d021 Mon Sep 17 00:00:00 2001 From: Untone Date: Wed, 31 Jan 2024 16:08:25 +0300 Subject: [PATCH 03/14] authorizer 2.0 code adaptation --- src/context/session.tsx | 57 +++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/src/context/session.tsx b/src/context/session.tsx index d637edb9..5cdd03e8 100644 --- a/src/context/session.tsx +++ b/src/context/session.tsx @@ -10,6 +10,8 @@ import { ConfigType, SignupInput, AuthorizeResponse, + ApiResponse, + GenericResponse, // GraphqlQueryInput, } from '@authorizerdev/authorizer-js' import { @@ -121,21 +123,31 @@ export const SessionProvider = (props: { // Function to load session data const sessionData = async () => { try { - const s = await authorizer().getSession() - console.info('[context.session] loading session', s) + const s: ApiResponse = await authorizer().getSession() + if (s?.data) { + console.info('[context.session] loading session', s) - // Set session expiration time in local storage - const expires_at = new Date(Date.now() + s.expires_in * 1000) - localStorage.setItem('expires_at', `${expires_at.getTime()}`) + // Set session expiration time in local storage + const expires_at = new Date(Date.now() + s.data.expires_in * 1000) + localStorage.setItem('expires_at', `${expires_at.getTime()}`) - // Set up session expiration check timer - minuteLater = setTimeout(checkSessionIsExpired, 60 * 1000) - console.info(`[context.session] will refresh in ${s.expires_in / 60} mins`) + // Set up session expiration check timer + minuteLater = setTimeout(checkSessionIsExpired, 60 * 1000) + console.info(`[context.session] will refresh in ${s.data.expires_in / 60} mins`) - // Set the session loaded flag - setIsSessionLoaded(true) + // Set the session loaded flag + setIsSessionLoaded(true) - return s + return s.data + } else { + console.info('[context.session] cannot refresh session', s.errors) + setAuthError(s.errors.pop().message) + + // Set the session loaded flag even if there's an error + setIsSessionLoaded(true) + + return null + } } catch (error) { console.info('[context.session] cannot refresh session', error) setAuthError(error) @@ -258,17 +270,20 @@ export const SessionProvider = (props: { // authorizer api proxy methods const signUp = async (params: SignupInput) => { - const authResult: void | AuthToken = await authorizer().signup(params) - if (authResult) setSession(authResult) + const authResult: ApiResponse = await authorizer().signup(params) + if (authResult?.data) setSession(authResult.data) + if (authResult?.errors) console.error(authResult.errors) } const signIn = async (params: LoginInput) => { - const authResult: AuthToken | void = await authorizer().login(params) - if (authResult) setSession(authResult) + const authResult: ApiResponse = await authorizer().login(params) + if (authResult?.data) setSession(authResult.data) + if (authResult?.errors) console.error(authResult.errors) } const signOut = async () => { - await authorizer().logout() + const authResult: ApiResponse = await authorizer().logout() + console.debug(authResult) reset() showSnackbar({ body: t("You've successfully logged out") }) } @@ -281,9 +296,13 @@ export const SessionProvider = (props: { const confirmEmail = async (input: VerifyEmailInput) => { console.debug(`[context.session] calling authorizer's verify email with`, input) try { - const at: void | AuthToken = await authorizer().verifyEmail(input) - if (at) setSession(at) - return at + const at: ApiResponse = await authorizer().verifyEmail(input) + if (at?.data) { + setSession(at.data) + return at.data + } else { + console.warn(at?.errors) + } } catch (error) { console.warn(error) } From cf19cdd39bddf6d7236229b2e91deaadb2b536e0 Mon Sep 17 00:00:00 2001 From: Untone Date: Wed, 31 Jan 2024 16:12:59 +0300 Subject: [PATCH 04/14] authorizer-upgrade --- src/components/Nav/AuthModal/ForgotPasswordForm.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx index 8dc12583..5fae5e8e 100644 --- a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx +++ b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx @@ -12,6 +12,7 @@ import { validateEmail } from '../../../utils/validateEmail' import { email, setEmail } from './sharedLogic' import styles from './AuthModal.module.scss' +import { ApiResponse, ForgotPasswordResponse } from '@authorizerdev/authorizer-js' type FormFields = { email: string @@ -61,12 +62,15 @@ export const ForgotPasswordForm = () => { setIsSubmitting(true) try { - const response = await authorizer().forgotPassword({ + const response: ApiResponse = await authorizer().forgotPassword({ email: email(), redirect_uri: window.location.origin, }) console.debug('[ForgotPasswordForm] authorizer response:', response) - if (response && response.message) setMessage(response.message) + if (response?.data) setMessage(response.data.message) + else { + console.warn(response.errors) + } } catch (error) { console.error(error) if (error?.code === 'user_not_found') { From 5a95e7490ecfaf4122570c9d9c863fd7df4c8ba7 Mon Sep 17 00:00:00 2001 From: Untone Date: Wed, 31 Jan 2024 16:24:40 +0300 Subject: [PATCH 05/14] authorizer-upgrade-3 --- .../Nav/AuthModal/ForgotPasswordForm.tsx | 29 +++++++++++-------- src/context/session.tsx | 13 ++++++++- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx index 5fae5e8e..1bf2b12d 100644 --- a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx +++ b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx @@ -12,7 +12,6 @@ import { validateEmail } from '../../../utils/validateEmail' import { email, setEmail } from './sharedLogic' import styles from './AuthModal.module.scss' -import { ApiResponse, ForgotPasswordResponse } from '@authorizerdev/authorizer-js' type FormFields = { email: string @@ -28,7 +27,7 @@ export const ForgotPasswordForm = () => { setEmail(newEmail.toLowerCase()) } const { - actions: { authorizer }, + actions: { forgotPassword }, } = useSession() const [submitError, setSubmitError] = createSignal('') const [isSubmitting, setIsSubmitting] = createSignal(false) @@ -62,22 +61,28 @@ export const ForgotPasswordForm = () => { setIsSubmitting(true) try { - const response: ApiResponse = await authorizer().forgotPassword({ + const { data, errors } = await forgotPassword({ email: email(), redirect_uri: window.location.origin, }) - console.debug('[ForgotPasswordForm] authorizer response:', response) - if (response?.data) setMessage(response.data.message) - else { - console.warn(response.errors) + if (data) { + console.debug('[ForgotPasswordForm] authorizer response:', data) + setMessage(data.message) + } + if (errors) { + console.warn(errors) + if (errors) { + const error: Error = errors[0] + if (error.cause === 'user_not_found') { + setIsUserNotFound(true) + return + } else { + setSubmitError(error.message) + } + } } } catch (error) { console.error(error) - if (error?.code === 'user_not_found') { - setIsUserNotFound(true) - return - } - setSubmitError(error?.message) } finally { setIsSubmitting(false) } diff --git a/src/context/session.tsx b/src/context/session.tsx index 5cdd03e8..c2b8b8f7 100644 --- a/src/context/session.tsx +++ b/src/context/session.tsx @@ -12,7 +12,8 @@ import { AuthorizeResponse, ApiResponse, GenericResponse, - // GraphqlQueryInput, + ForgotPasswordResponse, + ForgotPasswordInput, } from '@authorizerdev/authorizer-js' import { createContext, @@ -62,6 +63,9 @@ export type SessionContextType = { signIn: (params: LoginInput) => Promise signOut: () => Promise oauth: (provider: string) => Promise + forgotPassword: ( + params: ForgotPasswordInput, + ) => Promise<{ data: ForgotPasswordResponse; errors: Error[] }> changePassword: (password: string, token: string) => void confirmEmail: (input: VerifyEmailInput) => Promise // email confirm callback is in auth.discours.io setIsSessionLoaded: (loaded: boolean) => void @@ -293,6 +297,12 @@ export const SessionProvider = (props: { console.debug('[context.session] change password response:', resp) } + const forgotPassword = async (params: ForgotPasswordInput) => { + const resp = await authorizer().forgotPassword(params) + console.debug('[context.session] change password response:', resp) + return { data: resp?.data, errors: resp.errors } + } + const confirmEmail = async (input: VerifyEmailInput) => { console.debug(`[context.session] calling authorizer's verify email with`, input) try { @@ -338,6 +348,7 @@ export const SessionProvider = (props: { setAuthor, authorizer, loadAuthor, + forgotPassword, changePassword, oauth, } From af95e81c47396936478ece503489b21d543449a5 Mon Sep 17 00:00:00 2001 From: Untone Date: Wed, 31 Jan 2024 16:42:59 +0300 Subject: [PATCH 06/14] package-lock-fix --- package.json | 1 - src/components/Views/Author/Author.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/package.json b/package.json index 006bd345..09854bd3 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "@solid-primitives/pagination": "0.2.10", "cropperjs": "1.6.1", "form-data": "4.0.0", - "ga-gtag": "1.2.0", "i18next": "22.4.15", "i18next-icu": "2.3.0", "idb": "7.1.1", diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index b1eeebec..2742637b 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -127,7 +127,6 @@ export const AuthorView = (props: Props) => { // pagination if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) { - fetchData() loadMore() } }) From 90cf0a9e10b7189315bf70481a149dd98db3254d Mon Sep 17 00:00:00 2001 From: Untone Date: Wed, 31 Jan 2024 16:45:04 +0300 Subject: [PATCH 07/14] package-lock-fix-2 --- package-lock.json | 6 ++++++ package.json | 1 + 2 files changed, 7 insertions(+) diff --git a/package-lock.json b/package-lock.json index 5fb127b8..a56a55f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@solid-primitives/pagination": "0.2.10", "cropperjs": "1.6.1", "form-data": "4.0.0", + "ga-gtag": "1.2.0", "i18next": "22.4.15", "i18next-icu": "2.3.0", "idb": "7.1.1", @@ -9531,6 +9532,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ga-gtag": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ga-gtag/-/ga-gtag-1.2.0.tgz", + "integrity": "sha512-j9gxutMdpGMdwaX1SzOG31Ddm+IGFjeNf+N3Z5g+BBpS8FSXOALlrM+ORIGc/QKszGJEDlw+6PfIsJZICsqsGQ==" + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", diff --git a/package.json b/package.json index 09854bd3..006bd345 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@solid-primitives/pagination": "0.2.10", "cropperjs": "1.6.1", "form-data": "4.0.0", + "ga-gtag": "1.2.0", "i18next": "22.4.15", "i18next-icu": "2.3.0", "idb": "7.1.1", From 27711d7e99d2367d5f6447d85a54d16289905d21 Mon Sep 17 00:00:00 2001 From: Untone Date: Wed, 31 Jan 2024 16:54:43 +0300 Subject: [PATCH 08/14] forgot-form-fox --- .../Nav/AuthModal/ForgotPasswordForm.tsx | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx index 1bf2b12d..4e147d92 100644 --- a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx +++ b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx @@ -65,21 +65,16 @@ export const ForgotPasswordForm = () => { email: email(), redirect_uri: window.location.origin, }) - if (data) { - console.debug('[ForgotPasswordForm] authorizer response:', data) - setMessage(data.message) - } - if (errors) { - console.warn(errors) - if (errors) { - const error: Error = errors[0] - if (error.cause === 'user_not_found') { - setIsUserNotFound(true) - return - } else { - setSubmitError(error.message) - } - } + console.debug('[ForgotPasswordForm] authorizer response:', data) + setMessage(data.message) + + console.warn(errors) + if (errors.some((e) => e.cause === 'user_not_found')) { + setIsUserNotFound(true) + return + } else { + const errorText = errors.map((e) => e.message).join(' ') // FIXME + setSubmitError(errorText) } } catch (error) { console.error(error) From cfca2dbbabc56871fb8e18f5f0b450f4399dcd82 Mon Sep 17 00:00:00 2001 From: Untone Date: Wed, 31 Jan 2024 17:59:03 +0300 Subject: [PATCH 09/14] debug-subs --- src/context/following.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/context/following.tsx b/src/context/following.tsx index 2246485f..b22f8db1 100644 --- a/src/context/following.tsx +++ b/src/context/following.tsx @@ -80,17 +80,13 @@ export const FollowingProvider = (props: { children: JSX.Element }) => { } } - const loadData = (_a?: Author) => { - // console.debug('[context.following] current subs:', subscriptions) - if (!subscriptions?.authors?.length && !subscriptions?.topics?.length) { + createEffect(() => { + if (author() && !subscriptions?.authors?.length && !subscriptions?.topics?.length) { // && subscriptions.communites?.length + console.debug('[context.following] author with no subs detected') fetchData() } - } - createEffect(() => { - if (author()) loadData() }) - onMount(loadData) const setFollowing = (what: FollowingEntity, slug: string, value = true) => { setSubscriptions((prevSubscriptions) => { From 2103db3ebd03fdc33f9eaa661c34d1d9d871dcf0 Mon Sep 17 00:00:00 2001 From: Untone Date: Wed, 31 Jan 2024 18:15:23 +0300 Subject: [PATCH 10/14] load-comments-fix --- src/components/Views/Author/Author.tsx | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index 2742637b..b8813adb 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -144,22 +144,16 @@ export const AuthorView = (props: Props) => { } const [commented, setCommented] = createSignal([]) - createEffect( - on( - author, - (a: Author) => { - if (getPage().route === 'authorComments') { - console.debug('[components.Author] routed to comments') - try { - if (a) fetchComments(a) - } catch (error) { - console.error('[components.Author] fetch error', error) - } - } - }, - { defer: true }, - ), - ) + createEffect(() => { + if (author() && getPage().route === 'authorComments') { + console.debug('[components.Author] routed to comments') + try { + fetchComments(author()) + } catch (error) { + console.error('[components.Author] fetch error', error) + } + } + }) const ogImage = createMemo(() => author()?.pic From c234ab1c2bdbb10afc8e0d6079d678552806770c Mon Sep 17 00:00:00 2001 From: Untone Date: Wed, 31 Jan 2024 18:35:40 +0300 Subject: [PATCH 11/14] debug-detect --- src/context/following.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/context/following.tsx b/src/context/following.tsx index b22f8db1..163fd030 100644 --- a/src/context/following.tsx +++ b/src/context/following.tsx @@ -81,10 +81,13 @@ export const FollowingProvider = (props: { children: JSX.Element }) => { } createEffect(() => { - if (author() && !subscriptions?.authors?.length && !subscriptions?.topics?.length) { - // && subscriptions.communites?.length - console.debug('[context.following] author with no subs detected') - fetchData() + if (author()) { + console.debug('[context.following] author detect') + if (!subscriptions?.authors?.length && !subscriptions?.topics?.length) { + // && subscriptions.communites?.length + console.debug('[context.following] no subs detected') + fetchData() + } } }) From 8caa4f823a020bade6f0b21aea8db331f3b54aae Mon Sep 17 00:00:00 2001 From: Untone Date: Wed, 31 Jan 2024 18:46:34 +0300 Subject: [PATCH 12/14] detect-fix --- src/context/following.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/context/following.tsx b/src/context/following.tsx index 163fd030..c826740b 100644 --- a/src/context/following.tsx +++ b/src/context/following.tsx @@ -82,12 +82,8 @@ export const FollowingProvider = (props: { children: JSX.Element }) => { createEffect(() => { if (author()) { - console.debug('[context.following] author detect') - if (!subscriptions?.authors?.length && !subscriptions?.topics?.length) { - // && subscriptions.communites?.length - console.debug('[context.following] no subs detected') - fetchData() - } + console.debug('[context.following] author update detect') + fetchData() } }) From f6c64b1d46e5262eaa94d0f056df6d1bf3e0fe3c Mon Sep 17 00:00:00 2001 From: Untone Date: Wed, 31 Jan 2024 19:09:42 +0300 Subject: [PATCH 13/14] fetch-comments-fix --- src/components/Views/Author/Author.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index b8813adb..5d1e417e 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -25,6 +25,7 @@ import { Row3 } from '../../Feed/Row3' import styles from './Author.module.scss' import stylesArticle from '../../Article/Article.module.scss' +import { useFollowing } from '../../../context/following' type Props = { shouts: Shout[] @@ -37,6 +38,7 @@ const LOAD_MORE_PAGE_SIZE = 9 export const AuthorView = (props: Props) => { const { t } = useLocalize() + const { loadSubscriptions } = useFollowing() const { sortedArticles } = useArticlesStore({ shouts: props.shouts }) const { authorEntities } = useAuthorsStore({ authors: [props.author] }) const { page: getPage } = useRouter() @@ -128,6 +130,7 @@ export const AuthorView = (props: Props) => { // pagination if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) { loadMore() + loadSubscriptions() } }) @@ -145,13 +148,9 @@ export const AuthorView = (props: Props) => { const [commented, setCommented] = createSignal([]) createEffect(() => { - if (author() && getPage().route === 'authorComments') { - console.debug('[components.Author] routed to comments') - try { - fetchComments(author()) - } catch (error) { - console.error('[components.Author] fetch error', error) - } + const a = author() + if (a) { + fetchComments(a) } }) From 7e46bbdd9ebaed558cace80bd533b557666f39ac Mon Sep 17 00:00:00 2001 From: Untone Date: Wed, 31 Jan 2024 20:38:00 +0300 Subject: [PATCH 14/14] subs-fix --- src/components/Views/Author/Author.tsx | 2 +- src/context/inbox.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index 5d1e417e..2de280a3 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -5,6 +5,7 @@ import { Meta } from '@solidjs/meta' import { clsx } from 'clsx' import { Show, createMemo, createSignal, Switch, onMount, For, Match, createEffect, on } from 'solid-js' +import { useFollowing } from '../../../context/following' import { useLocalize } from '../../../context/localize' import { apiClient } from '../../../graphql/client/core' import { router, useRouter } from '../../../stores/router' @@ -25,7 +26,6 @@ import { Row3 } from '../../Feed/Row3' import styles from './Author.module.scss' import stylesArticle from '../../Article/Article.module.scss' -import { useFollowing } from '../../../context/following' type Props = { shouts: Shout[] diff --git a/src/context/inbox.tsx b/src/context/inbox.tsx index f518929c..22cbf22d 100644 --- a/src/context/inbox.tsx +++ b/src/context/inbox.tsx @@ -34,13 +34,13 @@ export const InboxProvider = (props: { children: JSX.Element }) => { const { sortedAuthors } = useAuthorsStore() const handleMessage = (sseMessage: SSEMessage) => { - console.log('[context.inbox]:', sseMessage) - // handling all action types: create update delete join left seen if (sseMessage.entity === 'message') { + console.debug('[context.inbox]:', sseMessage.payload) const relivedMessage = sseMessage.payload setMessages((prev) => [...prev, relivedMessage]) } else if (sseMessage.entity === 'chat') { + console.debug('[context.inbox]:', sseMessage.payload) const relivedChat = sseMessage.payload setChats((prev) => [...prev, relivedChat]) }