This commit is contained in:
Untone 2024-01-31 15:34:15 +03:00
parent cdcf9afcc2
commit 090a8f2633
30 changed files with 536 additions and 437 deletions

View File

@ -23,7 +23,7 @@ module.exports = {
}, },
extends: [ extends: [
'plugin:@typescript-eslint/recommended', '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: { rules: {
'@typescript-eslint/no-unused-vars': [ '@typescript-eslint/no-unused-vars': [
@ -40,12 +40,12 @@ module.exports = {
env: { env: {
browser: true, browser: true,
node: true, node: true,
mocha: true, // mocha: true,
}, },
globals: {}, globals: {},
rules: { rules: {
// Solid // Solid
'solid/reactivity': 'off', // too many 'should be used within JSX' 'solid/reactivity': 'off',
'solid/no-innerhtml': 'off', 'solid/no-innerhtml': 'off',
/** Unicorn **/ /** Unicorn **/
@ -65,6 +65,7 @@ module.exports = {
'unicorn/no-array-callback-reference': 'warn', 'unicorn/no-array-callback-reference': 'warn',
'unicorn/no-array-method-this-argument': 'warn', 'unicorn/no-array-method-this-argument': 'warn',
'unicorn/no-for-loop': 'off', 'unicorn/no-for-loop': 'off',
'unicorn/prefer-switch': 'warn',
'sonarjs/no-duplicate-string': ['warn', { threshold: 5 }], 'sonarjs/no-duplicate-string': ['warn', { threshold: 5 }],
'sonarjs/prefer-immediate-return': 'warn', 'sonarjs/prefer-immediate-return': 'warn',

1
.gitignore vendored
View File

@ -16,3 +16,4 @@ stats.html
*.scss.d.ts *.scss.d.ts
pnpm-lock.yaml pnpm-lock.yaml
bun.lockb bun.lockb
.jj

View File

@ -149,7 +149,7 @@
"typograf": "7.1.0", "typograf": "7.1.0",
"uniqolor": "1.1.0", "uniqolor": "1.1.0",
"vike": "0.4.148", "vike": "0.4.148",
"vite": "4.5.1", "vite": "4.5.2",
"vite-plugin-mkcert": "1.16.0", "vite-plugin-mkcert": "1.16.0",
"vite-plugin-sass-dts": "1.3.11", "vite-plugin-sass-dts": "1.3.11",
"vite-plugin-solid": "2.7.2", "vite-plugin-solid": "2.7.2",

View File

@ -7,6 +7,7 @@ import { Dynamic } from 'solid-js/web'
import { ConfirmProvider } from '../context/confirm' import { ConfirmProvider } from '../context/confirm'
import { ConnectProvider } from '../context/connect' import { ConnectProvider } from '../context/connect'
import { EditorProvider } from '../context/editor' import { EditorProvider } from '../context/editor'
import { FollowingProvider } from '../context/following'
import { InboxProvider } from '../context/inbox' import { InboxProvider } from '../context/inbox'
import { LocalizeProvider } from '../context/localize' import { LocalizeProvider } from '../context/localize'
import { MediaQueryProvider } from '../context/mediaQuery' import { MediaQueryProvider } from '../context/mediaQuery'
@ -90,7 +91,7 @@ type Props = PageProps & { is404: boolean }
export const App = (props: Props) => { export const App = (props: Props) => {
const { page, searchParams } = useRouter<RootSearchParams>() const { page, searchParams } = useRouter<RootSearchParams>()
let is404 = props.is404 const is404 = createMemo(() => props.is404)
createEffect(() => { createEffect(() => {
if (!searchParams().m) { if (!searchParams().m) {
@ -106,8 +107,7 @@ export const App = (props: Props) => {
const pageComponent = createMemo(() => { const pageComponent = createMemo(() => {
const result = pagesMap[page()?.route || 'home'] const result = pagesMap[page()?.route || 'home']
if (is404 || !result || page()?.path === '/404') { if (is404() || !result || page()?.path === '/404') {
is404 = false
return FourOuFourPage return FourOuFourPage
} }
@ -122,6 +122,7 @@ export const App = (props: Props) => {
<SnackbarProvider> <SnackbarProvider>
<ConfirmProvider> <ConfirmProvider>
<SessionProvider onStateChangeCallback={console.log}> <SessionProvider onStateChangeCallback={console.log}>
<FollowingProvider>
<ConnectProvider> <ConnectProvider>
<NotificationsProvider> <NotificationsProvider>
<EditorProvider> <EditorProvider>
@ -131,6 +132,7 @@ export const App = (props: Props) => {
</EditorProvider> </EditorProvider>
</NotificationsProvider> </NotificationsProvider>
</ConnectProvider> </ConnectProvider>
</FollowingProvider>
</SessionProvider> </SessionProvider>
</ConfirmProvider> </ConfirmProvider>
</SnackbarProvider> </SnackbarProvider>

View File

@ -2,13 +2,12 @@ import { openPage } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, Match, Show, Switch } from 'solid-js' import { createEffect, createMemo, createSignal, Match, Show, Switch } from 'solid-js'
import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useMediaQuery } from '../../../context/mediaQuery' import { useMediaQuery } from '../../../context/mediaQuery'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { Author, FollowingEntity } from '../../../graphql/schema/core.gen' import { Author, FollowingEntity } from '../../../graphql/schema/core.gen'
import { router, useRouter } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
import { follow, unfollow } from '../../../stores/zine/common'
// import { capitalize } from '../../../utils/capitalize'
import { isCyrillic } from '../../../utils/cyrillic' import { isCyrillic } from '../../../utils/cyrillic'
import { translit } from '../../../utils/ru2en' import { translit } from '../../../utils/ru2en'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
@ -33,7 +32,7 @@ type Props = {
export const AuthorBadge = (props: Props) => { export const AuthorBadge = (props: Props) => {
const { mediaMatches } = useMediaQuery() const { mediaMatches } = useMediaQuery()
const [isMobileView, setIsMobileView] = createSignal(false) const [isMobileView, setIsMobileView] = createSignal(false)
const [isSubscribing, setIsSubscribing] = createSignal(false) const [followed, setFollowed] = createSignal(false)
createEffect(() => { createEffect(() => {
setIsMobileView(!mediaMatches.sm) setIsMobileView(!mediaMatches.sm)
@ -41,33 +40,14 @@ export const AuthorBadge = (props: Props) => {
const { const {
author, author,
subscriptions, actions: { requireAuthentication },
actions: { loadSubscriptions, requireAuthentication },
} = useSession() } = useSession()
const { setFollowing } = useFollowing()
const { changeSearchParams } = useRouter() const { changeSearchParams } = useRouter()
const { t, formatDate, lang } = useLocalize() 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 = () => { const initChat = () => {
// eslint-disable-next-line solid/reactivity
requireAuthentication(() => { requireAuthentication(() => {
openPage(router, `inbox`) openPage(router, `inbox`)
changeSearchParams({ changeSearchParams({
@ -88,6 +68,14 @@ export const AuthorBadge = (props: Props) => {
return props.author.name return props.author.name
}) })
const handleFollowClick = () => {
const value = !followed()
requireAuthentication(() => {
setFollowed(value)
setFollowing(FollowingEntity.Author, props.author.slug, value)
}, 'subscribe')
}
return ( return (
<div class={clsx(styles.AuthorBadge, { [styles.nameOnly]: props.nameOnly })}> <div class={clsx(styles.AuthorBadge, { [styles.nameOnly]: props.nameOnly })}>
<div class={styles.basicInfo}> <div class={styles.basicInfo}>
@ -135,37 +123,24 @@ export const AuthorBadge = (props: Props) => {
<div class={styles.actions}> <div class={styles.actions}>
<Show <Show
when={!props.minimizeSubscribeButton} when={!props.minimizeSubscribeButton}
fallback={ fallback={<CheckButton text={t('Follow')} checked={followed()} onClick={handleFollowClick} />}
<CheckButton
text={t('Follow')}
checked={subscribed()}
onClick={() => handleSubscribe(!subscribed())}
/>
}
> >
<Show <Show
when={subscribed()} when={followed()}
fallback={ fallback={
<Button <Button
variant={props.iconButtons ? 'secondary' : 'bordered'} variant={props.iconButtons ? 'secondary' : 'bordered'}
size="S" size="S"
value={ value={
<Show <Show when={props.iconButtons} fallback={t('Subscribe')}>
when={props.iconButtons}
fallback={
<Show when={isSubscribing()} fallback={t('Subscribe')}>
{t('subscribing...')}
</Show>
}
>
<Icon name="author-subscribe" class={stylesButton.icon} /> <Icon name="author-subscribe" class={stylesButton.icon} />
</Show> </Show>
} }
onClick={() => handleSubscribe(true)} onClick={handleFollowClick}
isSubscribeButton={true} isSubscribeButton={true}
class={clsx(styles.actionButton, { class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons, [styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: subscribed(), [stylesButton.subscribed]: followed(),
})} })}
/> />
} }
@ -186,11 +161,11 @@ export const AuthorBadge = (props: Props) => {
<Icon name="author-unsubscribe" class={stylesButton.icon} /> <Icon name="author-unsubscribe" class={stylesButton.icon} />
</Show> </Show>
} }
onClick={() => handleSubscribe(false)} onClick={handleFollowClick}
isSubscribeButton={true} isSubscribeButton={true}
class={clsx(styles.actionButton, { class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons, [styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: subscribed(), [stylesButton.subscribed]: followed(),
})} })}
/> />
</Show> </Show>

View File

@ -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 { openPage, redirectPage } from '@nanostores/router'
import { clsx } from 'clsx' 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 { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen' import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
import { SubscriptionFilter } from '../../../pages/types' import { SubscriptionFilter } from '../../../pages/types'
import { router, useRouter } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
import { follow, unfollow } from '../../../stores/zine/common'
import { isCyrillic } from '../../../utils/cyrillic' import { isCyrillic } from '../../../utils/cyrillic'
import { isAuthor } from '../../../utils/isAuthor' import { isAuthor } from '../../../utils/isAuthor'
import { translit } from '../../../utils/ru2en' import { translit } from '../../../utils/ru2en'
@ -33,32 +33,14 @@ export const AuthorCard = (props: Props) => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const { const {
author, author,
subscriptions,
isSessionLoaded, isSessionLoaded,
actions: { loadSubscriptions, requireAuthentication }, actions: { requireAuthentication },
} = useSession() } = useSession()
const [authorSubs, setAuthorSubs] = createSignal<Array<Author | Topic | Community>>([])
const [isSubscribing, setIsSubscribing] = createSignal(false)
const [following, setFollowing] = createSignal<Array<Author | Topic>>(props.following)
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all') const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
const subscribed = createMemo<boolean>(() =>
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 isProfileOwner = createMemo(() => author()?.slug === props.author.slug)
const [followed, setFollowed] = createSignal()
const { setFollowing } = useFollowing()
const name = createMemo(() => { const name = createMemo(() => {
if (lang() !== 'ru' && isCyrillic(props.author.name)) { if (lang() !== 'ru' && isCyrillic(props.author.name)) {
if (props.author.name === 'Дискурс') { if (props.author.name === 'Дискурс') {
@ -71,9 +53,12 @@ export const AuthorCard = (props: Props) => {
return props.author.name return props.author.name
}) })
onMount(() => setAuthorSubs(props.following))
// TODO: reimplement AuthorCard // TODO: reimplement AuthorCard
const { changeSearchParams } = useRouter() const { changeSearchParams } = useRouter()
const initChat = () => { const initChat = () => {
// eslint-disable-next-line solid/reactivity
requireAuthentication(() => { requireAuthentication(() => {
openPage(router, `inbox`) openPage(router, `inbox`)
changeSearchParams({ changeSearchParams({
@ -82,30 +67,30 @@ export const AuthorCard = (props: Props) => {
}, 'discussions') }, 'discussions')
} }
const handleSubscribe = () => {
requireAuthentication(() => {
subscribe(!subscribed())
}, 'subscribe')
}
createEffect(() => { createEffect(() => {
if (props.following) { if (props.following) {
if (subscriptionFilter() === 'users') { if (subscriptionFilter() === 'authors') {
setFollowing(props.following.filter((s) => 'name' in s)) setAuthorSubs(props.following.filter((s) => 'name' in s))
} else if (subscriptionFilter() === 'topics') { } 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 { } else {
setFollowing(props.following) setAuthorSubs(props.following)
} }
} }
}) })
const followButtonText = createMemo(() => { const handleFollowClick = () => {
if (isSubscribing()) { const value = !followed()
return t('subscribing...') requireAuthentication(() => {
setFollowed(value)
setFollowing(FollowingEntity.Author, props.author.slug, value)
}, 'subscribe')
} }
if (subscribed()) { const followButtonText = createMemo(() => {
if (followed()) {
return ( return (
<> <>
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span> <span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
@ -214,11 +199,11 @@ export const AuthorCard = (props: Props) => {
fallback={ fallback={
<div class={styles.authorActions}> <div class={styles.authorActions}>
<Button <Button
onClick={handleSubscribe} onClick={handleFollowClick}
value={followButtonText()} value={followButtonText()}
isSubscribeButton={true} isSubscribeButton={true}
class={clsx({ class={clsx({
[stylesButton.subscribed]: subscribed(), [stylesButton.subscribed]: followed(),
})} })}
/> />
<Button <Button
@ -279,8 +264,8 @@ export const AuthorCard = (props: Props) => {
</button> </button>
<span class="view-switcher__counter">{props.following.length}</span> <span class="view-switcher__counter">{props.following.length}</span>
</li> </li>
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'users' })}> <li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'authors' })}>
<button type="button" onClick={() => setSubscriptionFilter('users')}> <button type="button" onClick={() => setSubscriptionFilter('authors')}>
{t('Authors')} {t('Authors')}
</button> </button>
<span class="view-switcher__counter"> <span class="view-switcher__counter">
@ -300,7 +285,7 @@ export const AuthorCard = (props: Props) => {
<div class={styles.listWrapper}> <div class={styles.listWrapper}>
<div class="row"> <div class="row">
<div class="col-24"> <div class="col-24">
<For each={following()}> <For each={authorSubs()}>
{(subscription) => {(subscription) =>
isAuthor(subscription) ? ( isAuthor(subscription) ? (
<AuthorBadge author={subscription} /> <AuthorBadge author={subscription} />

View File

@ -1,15 +1,10 @@
import type { Shout } from '../../graphql/schema/core.gen' 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 { ArticleCard } from './ArticleCard'
import { ArticleCardProps } from './ArticleCard/ArticleCard'
const x = [ const columnSizes = ['col-md-12', 'col-md-8', 'col-md-16']
['12', '12'],
['8', '16'],
['16', '8'],
]
export const Row2 = (props: { export const Row2 = (props: {
articles: Shout[] articles: Shout[]
@ -18,10 +13,10 @@ export const Row2 = (props: {
noAuthorLink?: boolean noAuthorLink?: boolean
noauthor?: boolean noauthor?: boolean
}) => { }) => {
const [y, setY] = createSignal(0) const [columnIndex, setColumnIndex] = createSignal(0)
// FIXME: random can break hydration // Update column index on component mount
createComputed(() => setY(Math.floor(Math.random() * x.length))) createEffect(() => setColumnIndex(Math.floor(Math.random() * columnSizes.length)))
return ( return (
<Show when={props.articles && props.articles.length > 0}> <Show when={props.articles && props.articles.length > 0}>
@ -29,31 +24,16 @@ export const Row2 = (props: {
<div class="wide-container"> <div class="wide-container">
<div class="row"> <div class="row">
<For each={props.articles}> <For each={props.articles}>
{(a, i) => { {(article, _idx) => {
// FIXME: refactor this, too ugly now const className = columnSizes[props.isEqual ? 0 : columnIndex() % columnSizes.length]
const className = `col-md-${props.isEqual ? '12' : x[y()][i()]}` const big = className === 'col-md-12' ? 'M' : 'L'
let desktopCoverSize: ArticleCardProps['desktopCoverSize'] const desktopCoverSize = className === 'col-md-8' ? 'S' : big
switch (className) {
case 'col-md-8': {
desktopCoverSize = 'S'
break
}
case 'col-md-12': {
desktopCoverSize = 'M'
break
}
default: {
desktopCoverSize = 'L'
}
}
return ( return (
<div class={className}> <div class={className}>
<ArticleCard <ArticleCard
article={a} article={article}
settings={{ settings={{
isWithCover: props.isEqual || x[y()][i()] === '16', isWithCover: props.isEqual || className === 'col-md-16',
nodate: props.isEqual || props.nodate, nodate: props.isEqual || props.nodate,
noAuthorLink: props.noAuthorLink, noAuthorLink: props.noAuthorLink,
noauthor: props.noauthor, noauthor: props.noauthor,

View File

@ -2,8 +2,9 @@ import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, For, Show } from 'solid-js' import { createSignal, For, Show } from 'solid-js'
import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { Author } from '../../../graphql/schema/core.gen'
import { router, useRouter } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
import { useArticlesStore } from '../../../stores/zine/articles' import { useArticlesStore } from '../../../stores/zine/articles'
import { useSeenStore } from '../../../stores/zine/seen' import { useSeenStore } from '../../../stores/zine/seen'
@ -15,7 +16,7 @@ import styles from './Sidebar.module.scss'
export const Sidebar = () => { export const Sidebar = () => {
const { t } = useLocalize() const { t } = useLocalize()
const { seen } = useSeenStore() const { seen } = useSeenStore()
const { subscriptions } = useSession() const { subscriptions } = useFollowing()
const { page } = useRouter() const { page } = useRouter()
const { articlesByTopic } = useArticlesStore() const { articlesByTopic } = useArticlesStore()
const [isSubscriptionsVisible, setSubscriptionsVisible] = createSignal(true) const [isSubscriptionsVisible, setSubscriptionsVisible] = createSignal(true)
@ -27,7 +28,6 @@ export const Sidebar = () => {
const checkAuthorIsSeen = (authorSlug: string) => { const checkAuthorIsSeen = (authorSlug: string) => {
return Boolean(seen()[authorSlug]) return Boolean(seen()[authorSlug])
} }
return ( return (
<div class={styles.sidebar}> <div class={styles.sidebar}>
<ul class={styles.feedFilters}> <ul class={styles.feedFilters}>
@ -111,7 +111,7 @@ export const Sidebar = () => {
</li> </li>
</ul> </ul>
<Show when={subscriptions().authors.length > 0 || subscriptions().topics.length > 0}> <Show when={subscriptions.authors.length > 0 || subscriptions.topics.length > 0}>
<h4 <h4
classList={{ [styles.opened]: isSubscriptionsVisible() }} classList={{ [styles.opened]: isSubscriptionsVisible() }}
onClick={() => { onClick={() => {
@ -122,22 +122,19 @@ export const Sidebar = () => {
</h4> </h4>
<ul class={clsx(styles.subscriptions, { [styles.hidden]: !isSubscriptionsVisible() })}> <ul class={clsx(styles.subscriptions, { [styles.hidden]: !isSubscriptionsVisible() })}>
<For each={subscriptions().authors}> <For each={subscriptions.authors}>
{(author) => ( {(a: Author) => (
<li> <li>
<a <a href={`/author/${a.slug}`} classList={{ [styles.unread]: checkAuthorIsSeen(a.slug) }}>
href={`/author/${author.slug}`}
classList={{ [styles.unread]: checkAuthorIsSeen(author.slug) }}
>
<div class={styles.sidebarItemName}> <div class={styles.sidebarItemName}>
<Userpic name={author.name} userpic={author.pic} size="XS" class={styles.userpic} /> <Userpic name={a.name} userpic={a.pic} size="XS" class={styles.userpic} />
<div class={styles.sidebarItemNameLabel}>{author.name}</div> <div class={styles.sidebarItemNameLabel}>{a.name}</div>
</div> </div>
</a> </a>
</li> </li>
)} )}
</For> </For>
<For each={subscriptions().topics}> <For each={subscriptions.topics}>
{(topic) => ( {(topic) => (
<li> <li>
<a <a

View File

@ -148,8 +148,10 @@ export const Header = (props: Props) => {
} }
onMount(async () => { onMount(async () => {
if (window.location.pathname === '/' || window.location.pathname === '') {
const topics = await apiClient.getRandomTopics({ amount: RANDOM_TOPICS_COUNT }) const topics = await apiClient.getRandomTopics({ amount: RANDOM_TOPICS_COUNT })
setRandomTopics(topics) setRandomTopics(topics)
}
}) })
const handleToggleMenuByLink = (event: MouseEvent, route: keyof typeof ROUTES) => { const handleToggleMenuByLink = (event: MouseEvent, route: keyof typeof ROUTES) => {

View File

@ -38,7 +38,7 @@ export const ProfileSettings = () => {
const [addLinkForm, setAddLinkForm] = createSignal<boolean>(false) const [addLinkForm, setAddLinkForm] = createSignal<boolean>(false)
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false) const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
const [isUserpicUpdating, setIsUserpicUpdating] = createSignal(false) const [isUserpicUpdating, setIsUserpicUpdating] = createSignal(false)
const [userpicFile, setUserpicFile] = createSignal<any | null>(null) const [userpicFile, setUserpicFile] = createSignal(null)
const [uploadError, setUploadError] = createSignal(false) const [uploadError, setUploadError] = createSignal(false)
const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false) const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false)
const [hostname, setHostname] = createSignal<string | null>(null) const [hostname, setHostname] = createSignal<string | null>(null)

View File

@ -1,12 +1,10 @@
import type { Topic } from '../../graphql/schema/core.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo, createSignal, Show } from 'solid-js' import { createMemo, createSignal, Show } from 'solid-js'
import { useFollowing } from '../../context/following'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { FollowingEntity } from '../../graphql/schema/core.gen' import { FollowingEntity, type Topic } from '../../graphql/schema/core.gen'
import { follow, unfollow } from '../../stores/zine/common'
import { capitalize } from '../../utils/capitalize' import { capitalize } from '../../utils/capitalize'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'
import { CheckButton } from '../_shared/CheckButton' import { CheckButton } from '../_shared/CheckButton'
@ -36,32 +34,21 @@ interface TopicProps {
export const TopicCard = (props: TopicProps) => { export const TopicCard = (props: TopicProps) => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const title = createMemo(() =>
capitalize(lang() === 'en' ? props.topic.slug.replaceAll('-', ' ') : props.topic.title || ''),
)
const { const {
subscriptions, author,
isSessionLoaded, actions: { requireAuthentication },
actions: { loadSubscriptions, requireAuthentication },
} = useSession() } = useSession()
const { setFollowing, loading: subLoading } = useFollowing()
const [followed, setFollowed] = createSignal()
const [isSubscribing, setIsSubscribing] = createSignal(false) const handleFollowClick = () => {
const value = !followed()
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 = () => {
requireAuthentication(() => { requireAuthentication(() => {
subscribe(!subscribed()) setFollowed(value)
setFollowing(FollowingEntity.Topic, props.topic.slug, value)
}, 'subscribe') }, 'subscribe')
} }
@ -69,12 +56,12 @@ export const TopicCard = (props: TopicProps) => {
return ( return (
<> <>
<Show when={props.iconButton}> <Show when={props.iconButton}>
<Show when={subscribed()} fallback="+"> <Show when={followed()} fallback="+">
<Icon name="check-subscribed" /> <Icon name="check-subscribed" />
</Show> </Show>
</Show> </Show>
<Show when={!props.iconButton}> <Show when={!props.iconButton}>
<Show when={subscribed()} fallback={t('Follow')}> <Show when={followed()} fallback={t('Follow')}>
<span class={stylesButton.buttonSubscribeLabelHovered}>{t('Unfollow')}</span> <span class={stylesButton.buttonSubscribeLabelHovered}>{t('Unfollow')}</span>
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span> <span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
</Show> </Show>
@ -83,10 +70,6 @@ export const TopicCard = (props: TopicProps) => {
) )
} }
const title = createMemo(() =>
capitalize(lang() === 'en' ? props.topic.slug.replaceAll('-', ' ') : props.topic.title || ''),
)
return ( return (
<div class={styles.topicContainer}> <div class={styles.topicContainer}>
<div <div
@ -141,24 +124,28 @@ export const TopicCard = (props: TopicProps) => {
}} }}
> >
<ShowOnlyOnClient> <ShowOnlyOnClient>
<Show when={isSessionLoaded()}> <Show when={author()}>
<Show <Show
when={!props.minimizeSubscribeButton} when={!props.minimizeSubscribeButton}
fallback={ fallback={
<CheckButton text={t('Follow')} checked={subscribed()} onClick={handleSubscribe} /> <CheckButton
text={t('Follow')}
checked={Boolean(followed())}
onClick={handleFollowClick}
/>
} }
> >
<Button <Button
variant="bordered" variant="bordered"
size="M" size="M"
value={subscribeValue()} value={subscribeValue()}
onClick={handleSubscribe} onClick={handleFollowClick}
isSubscribeButton={true} isSubscribeButton={true}
class={clsx(styles.actionButton, { class={clsx(styles.actionButton, {
[styles.isSubscribing]: isSubscribing(), [styles.isSubscribing]: subLoading(),
[stylesButton.subscribed]: subscribed(), [stylesButton.subscribed]: followed(),
})} })}
disabled={isSubscribing()} // disabled={subLoading()}
/> />
</Show> </Show>
</Show> </Show>

View File

@ -1,12 +1,12 @@
import type { Topic } from '../../graphql/schema/core.gen' import type { Topic } from '../../graphql/schema/core.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo, Show } from 'solid-js' import { createEffect, createSignal, Show } from 'solid-js'
import { useFollowing } from '../../context/following'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { FollowingEntity } from '../../graphql/schema/core.gen' import { FollowingEntity } from '../../graphql/schema/core.gen'
import { follow, unfollow } from '../../stores/zine/common'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'
import styles from './Full.module.scss' import styles from './Full.module.scss'
@ -16,23 +16,26 @@ type Props = {
} }
export const FullTopic = (props: Props) => { export const FullTopic = (props: Props) => {
const {
subscriptions,
actions: { requireAuthentication, loadSubscriptions },
} = useSession()
const { t } = useLocalize() const { t } = useLocalize()
const { subscriptions, setFollowing } = useFollowing()
const {
actions: { requireAuthentication },
} = useSession()
const [followed, setFollowed] = createSignal()
const subscribed = createMemo(() => createEffect(() => {
subscriptions().topics.some((topic) => topic.slug === props.topic?.slug), const subs = subscriptions
) if (subs?.topics.length !== 0) {
const items = subs.topics || []
setFollowed(items.some((x: Topic) => x?.slug === props.topic?.slug))
}
})
const handleSubscribe = (really: boolean) => { const handleFollowClick = (_ev) => {
requireAuthentication(async () => { const really = !followed()
await (really setFollowed(really)
? follow({ what: FollowingEntity.Topic, slug: props.topic.slug }) requireAuthentication(() => {
: unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug })) setFollowing(FollowingEntity.Topic, props.topic.slug, really)
loadSubscriptions()
}, 'follow') }, 'follow')
} }
@ -41,16 +44,11 @@ export const FullTopic = (props: Props) => {
<h1>#{props.topic?.title}</h1> <h1>#{props.topic?.title}</h1>
<p>{props.topic?.body}</p> <p>{props.topic?.body}</p>
<div class={clsx(styles.topicActions)}> <div class={clsx(styles.topicActions)}>
<Show when={!subscribed()}>
<Button variant="primary" onClick={() => handleSubscribe(true)} value={t('Follow the topic')} />
</Show>
<Show when={subscribed()}>
<Button <Button
variant="primary" variant="primary"
onClick={() => handleSubscribe(false)} onClick={handleFollowClick}
value={t('Unfollow the topic')} value={followed() ? t('Unfollow the topic') : t('Follow the topic')}
/> />
</Show>
<a class={styles.write} href={`/create/?topicId=${props.topic?.id}`}> <a class={styles.write} href={`/create/?topicId=${props.topic?.id}`}>
{t('Write about the topic')} {t('Write about the topic')}
</a> </a>

View File

@ -1,17 +1,18 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, Show } from 'solid-js' import { createEffect, createSignal, Show } from 'solid-js'
import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useMediaQuery } from '../../../context/mediaQuery' import { useMediaQuery } from '../../../context/mediaQuery'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen' import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
import { follow, unfollow } from '../../../stores/zine/common'
import { capitalize } from '../../../utils/capitalize' import { capitalize } from '../../../utils/capitalize'
import { getImageUrl } from '../../../utils/getImageUrl' import { getImageUrl } from '../../../utils/getImageUrl'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
import { CheckButton } from '../../_shared/CheckButton' import { CheckButton } from '../../_shared/CheckButton'
import styles from './TopicBadge.module.scss' import styles from './TopicBadge.module.scss'
type Props = { type Props = {
topic: Topic topic: Topic
minimizeSubscribeButton?: boolean minimizeSubscribeButton?: boolean
@ -21,29 +22,23 @@ export const TopicBadge = (props: Props) => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const { mediaMatches } = useMediaQuery() const { mediaMatches } = useMediaQuery()
const [isMobileView, setIsMobileView] = createSignal(false) const [isMobileView, setIsMobileView] = createSignal(false)
const [isSubscribing, setIsSubscribing] = createSignal(false) const {
actions: { requireAuthentication },
} = useSession()
const { setFollowing, loading: subLoading } = useFollowing()
const [followed, setFollowed] = createSignal()
const handleFollowClick = () => {
const value = !followed()
requireAuthentication(() => {
setFollowed(value)
setFollowing(FollowingEntity.Topic, props.topic.slug, value)
}, 'subscribe')
}
createEffect(() => { createEffect(() => {
setIsMobileView(!mediaMatches.sm) setIsMobileView(!mediaMatches.sm)
}) })
const {
subscriptions,
actions: { loadSubscriptions },
} = useSession()
const subscribed = createMemo(() =>
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 title = () => const title = () =>
lang() === 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title lang() === 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title
@ -82,23 +77,23 @@ export const TopicBadge = (props: Props) => {
<Show <Show
when={!props.minimizeSubscribeButton} when={!props.minimizeSubscribeButton}
fallback={ fallback={
<CheckButton text={t('Follow')} checked={subscribed()} onClick={() => subscribe(!subscribed)} /> <CheckButton text={t('Follow')} checked={Boolean(followed())} onClick={handleFollowClick} />
} }
> >
<Show <Show
when={subscribed()} when={followed()}
fallback={ fallback={
<Button <Button
variant="primary" variant="primary"
size="S" size="S"
value={isSubscribing() ? t('subscribing...') : t('Subscribe')} value={subLoading() ? t('subscribing...') : t('Subscribe')}
onClick={() => subscribe(true)} onClick={handleFollowClick}
class={styles.subscribeButton} class={styles.subscribeButton}
/> />
} }
> >
<Button <Button
onClick={() => subscribe(false)} onClick={handleFollowClick}
variant="bordered" variant="bordered"
size="S" size="S"
value={t('Following')} value={t('Following')}

View File

@ -4,8 +4,8 @@ import { Meta } from '@solidjs/meta'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js' import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
import { useFollowing } from '../../context/following'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useSession } from '../../context/session'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { setTopicsSort, useTopicsStore } from '../../stores/zine/topics' import { setTopicsSort, useTopicsStore } from '../../stores/zine/topics'
import { capitalize } from '../../utils/capitalize' import { capitalize } from '../../utils/capitalize'
@ -41,7 +41,7 @@ export const AllTopicsView = (props: Props) => {
sortBy: searchParams().by || 'shouts', sortBy: searchParams().by || 'shouts',
}) })
const { subscriptions } = useSession() const { subscriptions } = useFollowing()
createEffect(() => { createEffect(() => {
if (!searchParams().by) { if (!searchParams().by) {
@ -76,7 +76,7 @@ export const AllTopicsView = (props: Props) => {
return keys return keys
}) })
const subscribed = (topicSlug: string) => subscriptions().topics.some((topic) => topic.slug === topicSlug) const subscribed = (topicSlug: string) => subscriptions.topics.some((topic) => topic.slug === topicSlug)
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE) const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
const [searchQuery, setSearchQuery] = createSignal('') const [searchQuery, setSearchQuery] = createSignal('')
@ -191,7 +191,7 @@ export const AllTopicsView = (props: Props) => {
{(topic) => ( {(topic) => (
<> <>
<TopicCard <TopicCard
topic={topic as Topic} topic={topic}
compact={false} compact={false}
subscribed={subscribed(topic.slug)} subscribed={subscribed(topic.slug)}
showPublications={true} showPublications={true}

View File

@ -1,9 +1,9 @@
import type { Author, Shout, Topic } from '../../../graphql/schema/core.gen' import type { Author, Reaction, Shout, Topic } from '../../../graphql/schema/core.gen'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { Meta } from '@solidjs/meta' import { Meta } from '@solidjs/meta'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createMemo, createSignal, Switch, onMount, For, Match, createEffect } from 'solid-js' import { Show, createMemo, createSignal, Switch, onMount, For, Match, createEffect, on } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { apiClient } from '../../../graphql/client/core' import { apiClient } from '../../../graphql/client/core'
@ -13,6 +13,7 @@ import { loadAuthor, useAuthorsStore } from '../../../stores/zine/authors'
import { getImageUrl } from '../../../utils/getImageUrl' import { getImageUrl } from '../../../utils/getImageUrl'
import { getDescription } from '../../../utils/meta' import { getDescription } from '../../../utils/meta'
import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll' import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll'
import { byCreated } from '../../../utils/sortby'
import { splitToPages } from '../../../utils/splitToPages' import { splitToPages } from '../../../utils/splitToPages'
import { Loading } from '../../_shared/Loading' import { Loading } from '../../_shared/Loading'
import { Comment } from '../../Article/Comment' import { Comment } from '../../Article/Comment'
@ -44,21 +45,32 @@ export const AuthorView = (props: Props) => {
const [followers, setFollowers] = createSignal<Author[]>([]) const [followers, setFollowers] = createSignal<Author[]>([])
const [following, setFollowing] = createSignal<Array<Author | Topic>>([]) const [following, setFollowing] = createSignal<Array<Author | Topic>>([])
const [showExpandBioControl, setShowExpandBioControl] = createSignal(false) const [showExpandBioControl, setShowExpandBioControl] = createSignal(false)
const author = createMemo(() => authorEntities()[props.authorSlug])
createEffect(async () => { // current author
const [author, setAuthor] = createSignal<Author>()
createEffect(() => {
try {
const a = authorEntities()[props.authorSlug]
setAuthor(a)
} catch (error) {
console.debug(error)
}
})
createEffect(() => {
if (author() && author().id && !author().stat) { if (author() && author().id && !author().stat) {
const a = await loadAuthor({ slug: '', author_id: author().id }) const a = loadAuthor({ slug: '', author_id: author().id })
console.debug(`[AuthorView] loaded author:`, a) console.debug(`[AuthorView] loaded author:`, a)
} }
}) })
const bioContainerRef: { current: HTMLDivElement } = { current: null } const bioContainerRef: { current: HTMLDivElement } = { current: null }
const bioWrapperRef: { current: HTMLDivElement } = { current: null } const bioWrapperRef: { current: HTMLDivElement } = { current: null }
const fetchSubscriptions = async (): Promise<{ authors: Author[]; topics: Topic[] }> => { const fetchSubscriptions = async (): Promise<{ authors: Author[]; topics: Topic[] }> => {
try { try {
const [getAuthors, getTopics] = await Promise.all([ const [getAuthors, getTopics] = await Promise.all([
apiClient.getAuthorFollowingUsers({ slug: props.authorSlug }), apiClient.getAuthorFollowingAuthors({ slug: props.authorSlug }),
apiClient.getAuthorFollowingTopics({ slug: props.authorSlug }), apiClient.getAuthorFollowingTopics({ slug: props.authorSlug }),
]) ])
const authors = getAuthors const authors = getAuthors
@ -76,32 +88,27 @@ export const AuthorView = (props: Props) => {
} }
} }
onMount(async () => { const fetchData = async () => {
checkBioHeight() const slug = author()?.slug || props.authorSlug
if (slug && getPage().route === 'authorComments' && author()) {
// pagination
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
await loadMore()
}
})
createEffect(async () => {
const slug = author()?.slug
if (slug) {
console.debug('[AuthorView] load subscriptions')
try { try {
const { authors, topics } = await fetchSubscriptions() const { authors, topics } = await fetchSubscriptions()
setFollowing([...(authors || []), ...(topics || [])]) setFollowing([...(authors || []), ...(topics || [])])
const userSubscribers = await apiClient.getAuthorFollowers({ slug }) const flwrs = await apiClient.getAuthorFollowers({ slug })
setFollowers(userSubscribers || []) setFollowers(flwrs || [])
console.info('[components.Author] following data loaded')
} catch (error) { } catch (error) {
console.error('[AuthorView] error:', error) console.error('[components.Author] fetch error', error)
}
} }
} }
})
createEffect(() => { createEffect(() => {
document.title = author()?.name if (author()) {
console.info('[components.Author] profile data loaded')
document.title = author().name
fetchData()
}
}) })
const loadMore = async () => { const loadMore = async () => {
@ -115,42 +122,66 @@ export const AuthorView = (props: Props) => {
restoreScrollPosition() restoreScrollPosition()
} }
onMount(() => {
checkBioHeight()
// pagination
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
fetchData()
loadMore()
}
})
const pages = createMemo<Shout[][]>(() => const pages = createMemo<Shout[][]>(() =>
splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE), splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE),
) )
const [commented, setCommented] = createSignal([]) const fetchComments = async (commenter: Author) => {
createEffect(async () => {
if (getPage().route === 'authorComments' && props.author) {
try {
const data = await apiClient.getReactionsBy({ const data = await apiClient.getReactionsBy({
by: { comment: true, created_by: props.author.id }, by: { comment: true, created_by: commenter.id },
}) })
console.debug(`[components.Author] fetched ${data.length} comments`)
setCommented(data) setCommented(data)
} catch (error) {
console.error('[getReactionsBy comment]', error)
} }
}
})
const ogImage = props.author?.pic const [commented, setCommented] = createSignal<Reaction[]>([])
? getImageUrl(props.author.pic, { width: 1200 }) createEffect(
: getImageUrl('production/image/logo_image.png') on(
const description = getDescription(props.author?.bio) author,
const ogTitle = props.author?.name (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 },
),
)
const ogImage = createMemo(() =>
author()?.pic
? getImageUrl(author()?.pic, { width: 1200 })
: getImageUrl('production/image/logo_image.png'),
)
const description = createMemo(() => getDescription(author()?.bio))
return ( return (
<div class={styles.authorPage}> <div class={styles.authorPage}>
<Meta name="descprition" content={description} /> <Show when={author()}>
<Meta name="descprition" content={description()} />
<Meta name="og:type" content="profile" /> <Meta name="og:type" content="profile" />
<Meta name="og:title" content={ogTitle} /> <Meta name="og:title" content={author().name} />
<Meta name="og:image" content={ogImage} /> <Meta name="og:image" content={ogImage()} />
<Meta name="og:description" content={description} /> <Meta name="og:description" content={description()} />
<Meta name="twitter:card" content="summary_large_image" /> <Meta name="twitter:card" content="summary_large_image" />
<Meta name="twitter:title" content={ogTitle} /> <Meta name="twitter:title" content={author().name} />
<Meta name="twitter:description" content={description} /> <Meta name="twitter:description" content={description()} />
<Meta name="twitter:image" content={ogImage} /> <Meta name="twitter:image" content={ogImage()} />
</Show>
<div class="wide-container"> <div class="wide-container">
<Show when={author()} fallback={<Loading />}> <Show when={author()} fallback={<Loading />}>
<> <>

View File

@ -28,9 +28,10 @@ export const ProfileSubscriptions = () => {
const fetchSubscriptions = async () => { const fetchSubscriptions = async () => {
try { try {
const slug = author()?.slug
const [getAuthors, getTopics] = await Promise.all([ const [getAuthors, getTopics] = await Promise.all([
apiClient.getAuthorFollowingUsers({ slug: author()?.slug }), apiClient.getAuthorFollowingAuthors({ slug }),
apiClient.getAuthorFollowingTopics({ slug: author()?.slug }), apiClient.getAuthorFollowingTopics({ slug }),
]) ])
setFollowing([...getAuthors, ...getTopics]) setFollowing([...getAuthors, ...getTopics])
setFiltered([...getAuthors, ...getTopics]) setFiltered([...getAuthors, ...getTopics])
@ -42,7 +43,7 @@ export const ProfileSubscriptions = () => {
createEffect(() => { createEffect(() => {
if (following()) { if (following()) {
if (subscriptionFilter() === 'users') { if (subscriptionFilter() === 'authors') {
setFiltered(following().filter((s) => 'name' in s)) setFiltered(following().filter((s) => 'name' in s))
} else if (subscriptionFilter() === 'topics') { } else if (subscriptionFilter() === 'topics') {
setFiltered(following().filter((s) => 'title' in s)) setFiltered(following().filter((s) => 'title' in s))
@ -80,8 +81,8 @@ export const ProfileSubscriptions = () => {
{t('All')} {t('All')}
</button> </button>
</li> </li>
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'users' })}> <li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'authors' })}>
<button type="button" onClick={() => setSubscriptionFilter('users')}> <button type="button" onClick={() => setSubscriptionFilter('authors')}>
{t('Authors')} {t('Authors')}
</button> </button>
</li> </li>

View File

@ -1,6 +1,6 @@
import { redirectPage } from '@nanostores/router' import { redirectPage } from '@nanostores/router'
import { clsx } from 'clsx' 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 { createStore } from 'solid-js/store'
import { ShoutForm, useEditorContext } from '../../../context/editor' import { ShoutForm, useEditorContext } from '../../../context/editor'
@ -16,7 +16,6 @@ import { Icon } from '../../_shared/Icon'
import { Image } from '../../_shared/Image' import { Image } from '../../_shared/Image'
import { TopicSelect, UploadModalContent } from '../../Editor' import { TopicSelect, UploadModalContent } from '../../Editor'
import { Modal } from '../../Nav/Modal' import { Modal } from '../../Nav/Modal'
import { EMPTY_TOPIC } from '../Edit'
import styles from './PublishSettings.module.scss' import styles from './PublishSettings.module.scss'
import stylesBeside from '../../Feed/Beside.module.scss' import stylesBeside from '../../Feed/Beside.module.scss'
@ -36,21 +35,26 @@ const shorten = (str: string, maxLen: number) => {
return `${result}...` 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) => { export const PublishSettings = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { author } = useSession() const { author } = useSession()
const { sortedTopics } = useTopicsStore() const { sortedTopics } = useTopicsStore()
const [topics, setTopics] = createSignal<Topic[]>(sortedTopics()) const [topics, setTopics] = createSignal<Topic[]>(sortedTopics())
onMount(async () => {
await loadAllTopics()
})
createEffect(() => {
setTopics(sortedTopics())
})
const composeDescription = () => { const composeDescription = () => {
if (!props.form.description) { if (!props.form.description) {
const cleanFootnotes = props.form.body.replaceAll(/<footnote data-value=".*?">.*?<\/footnote>/g, '') const cleanFootnotes = props.form.body.replaceAll(/<footnote data-value=".*?">.*?<\/footnote>/g, '')
@ -60,23 +64,32 @@ export const PublishSettings = (props: Props) => {
return props.form.description return props.form.description
} }
const initialData: Partial<ShoutForm> = { const initialData = createMemo(() => {
coverImageUrl: props.form.coverImageUrl, return {
mainTopic: props.form.mainTopic || EMPTY_TOPIC, coverImageUrl: props.form?.coverImageUrl,
slug: props.form.slug, mainTopic: props.form?.mainTopic || EMPTY_TOPIC,
title: props.form.title, slug: props.form?.slug,
subtitle: props.form.subtitle, title: props.form?.title,
subtitle: props.form?.subtitle,
description: composeDescription(), description: composeDescription(),
selectedTopics: [], selectedTopics: [],
} }
})
const [settingsForm, setSettingsForm] = createStore(emptyConfig)
onMount(() => {
setSettingsForm(initialData())
loadAllTopics()
})
createEffect(() => setTopics(sortedTopics()))
const { const {
formErrors, formErrors,
actions: { setForm, setFormErrors, saveShout, publishShout }, actions: { setForm, setFormErrors, saveShout, publishShout },
} = useEditorContext() } = useEditorContext()
const [settingsForm, setSettingsForm] = createStore(initialData)
const handleUploadModalContentCloseSetCover = (image: UploadedFile) => { const handleUploadModalContentCloseSetCover = (image: UploadedFile) => {
hideModal() hideModal()
setSettingsForm('coverImageUrl', image.url) setSettingsForm('coverImageUrl', image.url)
@ -110,7 +123,7 @@ export const PublishSettings = (props: Props) => {
}) })
} }
const handleCancelClick = () => { const handleCancelClick = () => {
setSettingsForm(initialData) setSettingsForm(initialData())
handleBackClick() handleBackClick()
} }
const handlePublishSubmit = () => { const handlePublishSubmit = () => {
@ -149,9 +162,9 @@ export const PublishSettings = (props: Props) => {
[styles.hasImage]: settingsForm.coverImageUrl, [styles.hasImage]: settingsForm.coverImageUrl,
})} })}
> >
<Show when={settingsForm.coverImageUrl ?? initialData.coverImageUrl}> <Show when={settingsForm.coverImageUrl ?? initialData().coverImageUrl}>
<div class={styles.shoutCardCover}> <div class={styles.shoutCardCover}>
<Image src={settingsForm.coverImageUrl} alt={initialData.title} width={800} /> <Image src={settingsForm.coverImageUrl} alt={initialData().title} width={800} />
</div> </div>
</Show> </Show>
<div class={styles.text}> <div class={styles.text}>

View File

@ -2,7 +2,7 @@ import type { Shout, Topic } from '../../graphql/schema/core.gen'
import { Meta } from '@solidjs/meta' import { Meta } from '@solidjs/meta'
import { clsx } from 'clsx' 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 { useLocalize } from '../../context/localize'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
@ -39,23 +39,27 @@ const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3
export const TopicView = (props: Props) => { export const TopicView = (props: Props) => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const { searchParams, changeSearchParams } = useRouter<TopicsPageSearchParams>() const { searchParams, changeSearchParams } = useRouter<TopicsPageSearchParams>()
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const { sortedArticles } = useArticlesStore({ shouts: props.shouts }) const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
const { topicEntities } = useTopicsStore({ topics: [props.topic] }) const { topicEntities } = useTopicsStore({ topics: [props.topic] })
const { authorsByTopic } = useAuthorsStore() const { authorsByTopic } = useAuthorsStore()
const topic = createMemo(() => const [topic, setTopic] = createSignal<Topic>()
props.topic?.slug in topicEntities() ? topicEntities()[props.topic.slug] : props.topic, createEffect(() => {
) const topics = topicEntities()
const title = () => if (props.topicSlug && !topic() && topics) {
setTopic(topics[props.topicSlug])
}
})
const title = createMemo(
() =>
`#${capitalize( `#${capitalize(
lang() === 'en' ? topic()?.slug.replace(/-/, ' ') : topic()?.title || topic()?.slug.replace(/-/, ' '), lang() === 'en'
? topic()?.slug.replace(/-/, ' ')
: topic()?.title || topic()?.slug.replace(/-/, ' '),
true, true,
)}` )}`,
onMount(() => (document.title = title())) )
const loadMore = async () => { const loadMore = async () => {
saveScrollPosition() saveScrollPosition()

127
src/context/following.tsx Normal file
View File

@ -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<boolean>
subscriptions: SubscriptionsData
setSubscriptions: (subscriptions: SubscriptionsData) => void
setFollowing: (what: FollowingEntity, slug: string, value: boolean) => void
loadSubscriptions: () => void
follow: (what: FollowingEntity, slug: string) => Promise<void>
unfollow: (what: FollowingEntity, slug: string) => Promise<void>
}
const FollowingContext = createContext<FollowingContextType>()
export function useFollowing() {
return useContext(FollowingContext)
}
const EMPTY_SUBSCRIPTIONS = {
topics: [],
authors: [],
communities: [],
}
export const FollowingProvider = (props: { children: JSX.Element }) => {
const [loading, setLoading] = createSignal<boolean>(false)
const [subscriptions, setSubscriptions] = createStore<SubscriptionsData>(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 <FollowingContext.Provider value={value}>{props.children}</FollowingContext.Provider>
}

View File

@ -56,7 +56,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
const createReaction = async (input: ReactionInput): Promise<void> => { const createReaction = async (input: ReactionInput): Promise<void> => {
const reaction = await apiClient.createReaction(input) const reaction = await apiClient.createReaction(input)
if (!reaction) return
const changes = { const changes = {
[reaction.id]: reaction, [reaction.id]: reaction,
} }

View File

@ -1,5 +1,5 @@
import type { AuthModalSource } from '../components/Nav/AuthModal/types' 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 type { Accessor, JSX, Resource } from 'solid-js'
import { import {
@ -41,19 +41,17 @@ const defaultConfig: ConfigType = {
} }
export type SessionContextType = { export type SessionContextType = {
config: ConfigType config: Accessor<ConfigType>
session: Resource<AuthToken> session: Resource<AuthToken>
author: Resource<Author | null> author: Resource<Author | null>
authError: Accessor<string> authError: Accessor<string>
isSessionLoaded: Accessor<boolean> isSessionLoaded: Accessor<boolean>
subscriptions: Accessor<Result>
isAuthenticated: Accessor<boolean> isAuthenticated: Accessor<boolean>
actions: { actions: {
loadSession: () => AuthToken | Promise<AuthToken> loadSession: () => AuthToken | Promise<AuthToken>
setSession: (token: AuthToken | null) => void // setSession setSession: (token: AuthToken | null) => void // setSession
loadAuthor: (info?: unknown) => Author | Promise<Author> loadAuthor: (info?: unknown) => Author | Promise<Author>
setAuthor: (a: Author) => void setAuthor: (a: Author) => void
loadSubscriptions: () => Promise<void>
requireAuthentication: ( requireAuthentication: (
callback: (() => Promise<void>) | (() => void), callback: (() => Promise<void>) | (() => void),
modalSource: AuthModalSource, modalSource: AuthModalSource,
@ -77,11 +75,6 @@ export function useSession() {
return useContext(SessionContext) return useContext(SessionContext)
} }
const EMPTY_SUBSCRIPTIONS = {
topics: [],
authors: [],
}
export const SessionProvider = (props: { export const SessionProvider = (props: {
onStateChangeCallback(state: AuthToken): unknown onStateChangeCallback(state: AuthToken): unknown
children: JSX.Element children: JSX.Element
@ -91,12 +84,12 @@ export const SessionProvider = (props: {
actions: { showSnackbar }, actions: { showSnackbar },
} = useSnackbar() } = useSnackbar()
const { searchParams, changeSearchParams } = useRouter() const { searchParams, changeSearchParams } = useRouter()
const [configuration, setConfig] = createSignal<ConfigType>(defaultConfig) const [config, setConfig] = createSignal<ConfigType>(defaultConfig)
const authorizer = createMemo(() => new Authorizer(configuration())) const authorizer = createMemo(() => new Authorizer(config()))
const [oauthState, setOauthState] = createSignal<string>() const [oauthState, setOauthState] = createSignal<string>()
// handle callback's redirect_uri // handle callback's redirect_uri
createEffect(async () => { createEffect(() => {
// oauth // oauth
const state = searchParams()?.state const state = searchParams()?.state
if (state) { if (state) {
@ -120,12 +113,13 @@ export const SessionProvider = (props: {
}) })
// load // load
let minuteLater let minuteLater: NodeJS.Timeout | null
const [isSessionLoaded, setIsSessionLoaded] = createSignal(false) const [isSessionLoaded, setIsSessionLoaded] = createSignal(false)
const [authError, setAuthError] = createSignal('') const [authError, setAuthError] = createSignal('')
const [session, { refetch: loadSession, mutate: setSession }] = createResource<AuthToken>(
async () => { // Function to load session data
const sessionData = async () => {
try { try {
const s = await authorizer().getSession() const s = await authorizer().getSession()
console.info('[context.session] loading session', s) console.info('[context.session] loading session', s)
@ -151,12 +145,12 @@ export const SessionProvider = (props: {
return null return null
} }
}, }
{
const [session, { refetch: loadSession, mutate: setSession }] = createResource<AuthToken>(sessionData, {
ssrLoadFrom: 'initial', ssrLoadFrom: 'initial',
initialValue: null, initialValue: null,
}, })
)
const checkSessionIsExpired = () => { const checkSessionIsExpired = () => {
const expires_at_data = localStorage.getItem('expires_at') const expires_at_data = localStorage.getItem('expires_at')
@ -177,26 +171,17 @@ export const SessionProvider = (props: {
} }
onCleanup(() => clearTimeout(minuteLater)) onCleanup(() => clearTimeout(minuteLater))
const authorData = async () => {
const [author, { refetch: loadAuthor, mutate: setAuthor }] = createResource<Author | null>(
async () => {
const u = session()?.user const u = session()?.user
return u ? (await apiClient.getAuthorId({ user: u.id.trim() })) || null : null return u ? (await apiClient.getAuthorId({ user: u.id.trim() })) || null : null
}, }
{ const [author, { refetch: loadAuthor, mutate: setAuthor }] = createResource<Author | null>(authorData, {
ssrLoadFrom: 'initial', ssrLoadFrom: 'initial',
initialValue: null, initialValue: null,
}, })
)
const [subscriptions, setSubscriptions] = createSignal<Result>(EMPTY_SUBSCRIPTIONS)
const loadSubscriptions = async (): Promise<void> => {
const result = await apiClient.getMySubscriptions()
setSubscriptions(result || EMPTY_SUBSCRIPTIONS)
}
// when session is loaded // when session is loaded
createEffect(async () => { createEffect(() => {
if (session()) { if (session()) {
const token = session()?.access_token const token = session()?.access_token
if (token) { if (token) {
@ -206,23 +191,24 @@ export const SessionProvider = (props: {
notifierClient.connect(token) notifierClient.connect(token)
inboxClient.connect(token) inboxClient.connect(token)
} }
if (!author()) { if (!author()) loadAuthor()
const a = await loadAuthor()
if (a) {
await loadSubscriptions()
addAuthors([a])
} else {
reset()
}
}
setIsSessionLoaded(true) setIsSessionLoaded(true)
} }
} }
}) })
// when author is loaded
createEffect(() => {
if (author()) {
addAuthors([author()])
} else {
reset()
}
})
const reset = () => { const reset = () => {
setIsSessionLoaded(true) setIsSessionLoaded(true)
setSubscriptions(EMPTY_SUBSCRIPTIONS)
setSession(null) setSession(null)
setAuthor(null) setAuthor(null)
} }
@ -252,10 +238,10 @@ export const SessionProvider = (props: {
) )
const [authCallback, setAuthCallback] = createSignal<() => void>(() => {}) const [authCallback, setAuthCallback] = createSignal<() => void>(() => {})
const requireAuthentication = async (callback: () => void, modalSource: AuthModalSource) => { const requireAuthentication = (callback: () => void, modalSource: AuthModalSource) => {
setAuthCallback((_cb) => callback) setAuthCallback((_cb) => callback)
if (!session()) { if (!session()) {
await loadSession() loadSession()
if (!session()) { if (!session()) {
showModal('auth', modalSource) showModal('auth', modalSource)
} }
@ -323,7 +309,6 @@ export const SessionProvider = (props: {
const isAuthenticated = createMemo(() => Boolean(author())) const isAuthenticated = createMemo(() => Boolean(author()))
const actions = { const actions = {
loadSession, loadSession,
loadSubscriptions,
requireAuthentication, requireAuthentication,
signUp, signUp,
signIn, signIn,
@ -339,9 +324,8 @@ export const SessionProvider = (props: {
} }
const value: SessionContextType = { const value: SessionContextType = {
authError, authError,
config: configuration(), config,
session, session,
subscriptions,
isSessionLoaded, isSessionLoaded,
author, author,
actions, actions,

View File

@ -12,6 +12,7 @@ import type {
QueryLoad_Authors_ByArgs, QueryLoad_Authors_ByArgs,
QueryLoad_Shouts_SearchArgs, QueryLoad_Shouts_SearchArgs,
QueryLoad_Shouts_Random_TopArgs, QueryLoad_Shouts_Random_TopArgs,
Community,
} from '../schema/core.gen' } from '../schema/core.gen'
import { createGraphQLClient } from '../createGraphQLClient' import { createGraphQLClient } from '../createGraphQLClient'
@ -37,20 +38,24 @@ import authorBy from '../query/core/author-by'
import authorFollowers from '../query/core/author-followers' import authorFollowers from '../query/core/author-followers'
import authorId from '../query/core/author-id' import authorId from '../query/core/author-id'
import authorsAll from '../query/core/authors-all' import authorsAll from '../query/core/authors-all'
import authorFollowed from '../query/core/authors-followed-by'
import authorsLoadBy from '../query/core/authors-load-by' import authorsLoadBy from '../query/core/authors-load-by'
import mySubscriptions from '../query/core/my-followed' import mySubscriptions from '../query/core/my-followed'
import reactionsLoadBy from '../query/core/reactions-load-by' import reactionsLoadBy from '../query/core/reactions-load-by'
import topicBySlug from '../query/core/topic-by-slug' import topicBySlug from '../query/core/topic-by-slug'
import topicsAll from '../query/core/topics-all' 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' import topicsRandomQuery from '../query/core/topics-random'
const publicGraphQLClient = createGraphQLClient('core') const publicGraphQLClient = createGraphQLClient('core')
export const apiClient = { export const apiClient = {
private: null, 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) => { getRandomTopShouts: async (params: QueryLoad_Shouts_Random_TopArgs) => {
const response = await publicGraphQLClient.query(loadShoutsTopRandom, params).toPromise() const response = await publicGraphQLClient.query(loadShoutsTopRandom, params).toPromise()
@ -119,14 +124,18 @@ export const apiClient = {
const response = await publicGraphQLClient.query(authorFollowers, { slug }).toPromise() const response = await publicGraphQLClient.query(authorFollowers, { slug }).toPromise()
return response.data.get_author_followers return response.data.get_author_followers
}, },
getAuthorFollowingUsers: async ({ slug }: { slug: string }): Promise<Author[]> => { getAuthorFollowingAuthors: async ({ slug }: { slug: string }): Promise<Author[]> => {
const response = await publicGraphQLClient.query(authorFollowed, { slug }).toPromise() const response = await publicGraphQLClient.query(authorFollowedAuthors, { slug }).toPromise()
return response.data.get_author_followed return response.data.get_author_followed
}, },
getAuthorFollowingTopics: async ({ slug }: { slug: string }): Promise<Topic[]> => { getAuthorFollowingTopics: async ({ slug }: { slug: string }): Promise<Topic[]> => {
const response = await publicGraphQLClient.query(userFollowedTopics, { slug }).toPromise() const response = await publicGraphQLClient.query(authorFollowedTopics, { slug }).toPromise()
return response.data.get_topics_by_author return response.data.get_topics_by_author
}, },
getAuthorFollowingCommunities: async ({ slug }: { slug: string }): Promise<Community[]> => {
const response = await publicGraphQLClient.query(authorFollowedCommunities, { slug }).toPromise()
return response.data.get_communities_by_author
},
updateProfile: async (input: ProfileInput) => { updateProfile: async (input: ProfileInput) => {
const response = await apiClient.private.mutation(updateProfile, { profile: input }).toPromise() const response = await apiClient.private.mutation(updateProfile, { profile: input }).toPromise()
return response.data.update_profile return response.data.update_profile
@ -200,7 +209,7 @@ export const apiClient = {
const resp = await publicGraphQLClient.query(shoutsLoadBy, { options }).toPromise() const resp = await publicGraphQLClient.query(shoutsLoadBy, { options }).toPromise()
if (resp.error) console.error(resp) 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) => { getShoutsSearch: async ({ text, limit, offset }: QueryLoad_Shouts_SearchArgs) => {

View File

@ -8,7 +8,6 @@ export default gql`
id id
body body
kind kind
range
created_at created_at
reply_to reply_to
stat { stat {

View File

@ -1,7 +1,7 @@
import { gql } from '@urql/core' import { gql } from '@urql/core'
export default gql` 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) { load_authors_by(by: $by, limit: $limit, offset: $offset) {
id id
slug slug

View File

@ -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
}
}
}
`

View File

@ -1,7 +1,7 @@
import { gql } from '@urql/core' import { gql } from '@urql/core'
export default gql` 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) { get_topics_by_author(slug: $slug, user: $user, author_id: $author_id) {
id id
slug slug

View File

@ -50,4 +50,4 @@ export type UploadedFile = {
originalFilename?: string originalFilename?: string
} }
export type SubscriptionFilter = 'all' | 'users' | 'topics' export type SubscriptionFilter = 'all' | 'authors' | 'topics' | 'communities'

View File

@ -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 })
}

View File

@ -15,7 +15,7 @@ export const getImageUrl = (
src: string, src: string,
options: { width?: number; height?: number; noSizeUrlPart?: boolean } = {}, 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 isAudio = src.toLowerCase().split('.').pop() in ['wav', 'mp3', 'ogg', 'aif', 'flac']
const base = isAudio ? cdnUrl : `${thumborUrl}/unsafe/` const base = isAudio ? cdnUrl : `${thumborUrl}/unsafe/`
const sizeUrlPart = isAudio ? '' : getSizeUrlPart(options) const sizeUrlPart = isAudio ? '' : getSizeUrlPart(options)

View File

@ -1,9 +1,9 @@
import { onCleanup, onMount } from 'solid-js' import { onCleanup, onMount } from 'solid-js'
export const useEscKeyDownHandler = (onEscKeyDown: () => void) => { export const useEscKeyDownHandler = (onEscKeyDown: (ev) => void) => {
const keydownHandler = (e: KeyboardEvent) => { const keydownHandler = (e: KeyboardEvent) => {
if (e.key === 'Escape') { if (e.key === 'Escape') {
onEscKeyDown() onEscKeyDown(e)
} }
} }