Merge pull request #403 from Discours/hotfix/topicSubscibtionsFix

Fix topic subscriptions status
This commit is contained in:
Tony 2024-02-08 17:25:08 +03:00 committed by GitHub
commit b3e3068a8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 152 additions and 137 deletions

View File

@ -16,13 +16,10 @@ import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { Userpic } from '../Userpic' import { Userpic } from '../Userpic'
import { FollowedInfo } from '../../../pages/types'
import stylesButton from '../../_shared/Button/Button.module.scss' import stylesButton from '../../_shared/Button/Button.module.scss'
import styles from './AuthorBadge.module.scss' import styles from './AuthorBadge.module.scss'
type FollowedInfo = {
value?: boolean
loaded?: boolean
}
type Props = { type Props = {
author: Author author: Author
minimizeSubscribeButton?: boolean minimizeSubscribeButton?: boolean

View File

@ -308,7 +308,13 @@ export const AuthorCard = (props: Props) => {
author={subscription} author={subscription}
/> />
) : ( ) : (
<TopicBadge topic={subscription} /> <TopicBadge
isFollowed={{
loaded: Boolean(authorSubs()),
value: isOwnerSubscribed(subscription.id),
}}
topic={subscription}
/>
) )
} }
</For> </For>

View File

@ -1,10 +1,13 @@
.TopicBadge { .TopicBadge {
display: flex;
flex-direction: row;
align-items: flex-start;
margin-bottom: 2rem; margin-bottom: 2rem;
gap: 1rem; gap: 1rem;
.content {
align-items: flex-start;
display: flex;
flex-direction: row;
margin-bottom: .8rem;
}
.basicInfo { .basicInfo {
display: flex; display: flex;
flex-flow: row nowrap; flex-flow: row nowrap;
@ -78,3 +81,34 @@
width: 9em; width: 9em;
} }
} }
.stats {
@include font-size(1.5rem);
color: var(--secondary-color);
display: flex;
margin: 0 0 1em;
@include media-breakpoint-down(md) {
flex-wrap: wrap;
}
@include media-breakpoint-down(sm) {
margin-top: 0.5em;
}
.statsItem {
@include font-size(1.4rem);
margin-right: 1.6rem;
white-space: nowrap;
&:last-child {
margin-right: 0;
}
&.followers {
word-break: keep-all;
}
}
}

View File

@ -1,5 +1,5 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createEffect, createSignal } from 'solid-js' import { Show, createEffect, createSignal, on } from 'solid-js'
import { useFollowing } from '../../../context/following' import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
@ -11,11 +11,14 @@ 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 { FollowedInfo } from '../../../pages/types'
import styles from './TopicBadge.module.scss' import styles from './TopicBadge.module.scss'
type Props = { type Props = {
topic: Topic topic: Topic
minimizeSubscribeButton?: boolean minimizeSubscribeButton?: boolean
isFollowed?: FollowedInfo
showStat?: boolean
} }
export const TopicBadge = (props: Props) => { export const TopicBadge = (props: Props) => {
@ -24,12 +27,12 @@ export const TopicBadge = (props: Props) => {
const [isMobileView, setIsMobileView] = createSignal(false) const [isMobileView, setIsMobileView] = createSignal(false)
const { requireAuthentication } = useSession() const { requireAuthentication } = useSession()
const { setFollowing, loading: subLoading } = useFollowing() const { setFollowing, loading: subLoading } = useFollowing()
const [followed, setFollowed] = createSignal() const [isFollowed, setIsFollowed] = createSignal<boolean>()
const handleFollowClick = () => { const handleFollowClick = () => {
const value = !followed() const value = !isFollowed()
requireAuthentication(() => { requireAuthentication(() => {
setFollowed(value) setIsFollowed(value)
setFollowing(FollowingEntity.Topic, props.topic.slug, value) setFollowing(FollowingEntity.Topic, props.topic.slug, value)
}, 'subscribe') }, 'subscribe')
} }
@ -38,67 +41,85 @@ export const TopicBadge = (props: Props) => {
setIsMobileView(!mediaMatches.sm) setIsMobileView(!mediaMatches.sm)
}) })
createEffect(
on(
() => props.isFollowed,
() => {
setIsFollowed(props.isFollowed.value)
},
),
)
const title = () => const title = () =>
lang() === 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title lang() === 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title
return ( return (
<div class={styles.TopicBadge}> <div class={styles.TopicBadge}>
<div class={styles.basicInfo}> <div class={styles.content}>
<a <div class={styles.basicInfo}>
href={`/topic/${props.topic.slug}`} <a
class={clsx(styles.picture, { href={`/topic/${props.topic.slug}`}
[styles.withImage]: props.topic.pic, class={clsx(styles.picture, {
[styles.smallSize]: isMobileView(), [styles.withImage]: props.topic.pic,
})} [styles.smallSize]: isMobileView(),
style={ })}
props.topic.pic && { style={
'background-image': `url('${getImageUrl(props.topic.pic, { width: 40, height: 40 })}')`, props.topic.pic && {
'background-image': `url('${getImageUrl(props.topic.pic, { width: 40, height: 40 })}')`,
}
} }
} />
/> <a href={`/topic/${props.topic.slug}`} class={styles.info}>
<a href={`/topic/${props.topic.slug}`} class={styles.info}> <span class={styles.title}>{title()}</span>
<span class={styles.title}>{title()}</span> <Show
when={props.topic.body}
fallback={
<div class={styles.description}>
{t('PublicationsWithCount', { count: props.topic.stat.shouts ?? 0 })}
</div>
}
>
<div class={clsx('text-truncate', styles.description)}>{props.topic.body}</div>
</Show>
</a>
</div>
<div class={styles.actions}>
<Show <Show
when={props.topic.body} when={!props.minimizeSubscribeButton}
fallback={ fallback={
<div class={styles.description}> <CheckButton text={t('Follow')} checked={Boolean(isFollowed())} onClick={handleFollowClick} />
{t('PublicationsWithCount', { count: props.topic.stat.shouts ?? 0 })}
</div>
} }
> >
<div class={clsx('text-truncate', styles.description)}>{props.topic.body}</div> <Show
</Show> when={isFollowed()}
</a> fallback={
</div> <Button
variant="primary"
<div class={styles.actions}> size="S"
<Show value={subLoading() ? t('subscribing...') : t('Subscribe')}
when={!props.minimizeSubscribeButton} onClick={handleFollowClick}
fallback={ class={styles.subscribeButton}
<CheckButton text={t('Follow')} checked={Boolean(followed())} onClick={handleFollowClick} /> />
} }
> >
<Show
when={followed()}
fallback={
<Button <Button
variant="primary"
size="S"
value={subLoading() ? t('subscribing...') : t('Subscribe')}
onClick={handleFollowClick} onClick={handleFollowClick}
variant="bordered"
size="S"
value={t('Following')}
class={styles.subscribeButton} class={styles.subscribeButton}
/> />
} </Show>
>
<Button
onClick={handleFollowClick}
variant="bordered"
size="S"
value={t('Following')}
class={styles.subscribeButton}
/>
</Show> </Show>
</Show> </div>
</div>
<div class={styles.stats}>
<span class={styles.statsItem}>{t('shoutsWithCount', { count: props.topic?.stat?.shouts })}</span>
<span class={styles.statsItem}>{t('authorsWithCount', { count: props.topic?.stat?.authors })}</span>
<span class={styles.statsItem}>
{t('followersWithCount', { count: props.topic?.stat?.followers })}
</span>
</div> </div>
</div> </div>
) )

View File

@ -32,45 +32,6 @@
} }
} }
.stats {
@include font-size(1.7rem);
color: #9fa1a7;
display: flex;
margin: 0 0 1em;
@include media-breakpoint-down(md) {
flex-wrap: wrap;
}
@include media-breakpoint-down(sm) {
margin-top: 0.5em;
}
.statsItem {
@include font-size(1.5rem);
margin-right: 1.6rem;
white-space: nowrap;
&:last-child {
margin-right: 0;
}
&.compact {
font-size: small;
}
&.followers {
word-break: keep-all;
}
&.button {
float: right;
}
}
}
.loadMoreContainer { .loadMoreContainer {
margin-top: 48px; margin-top: 48px;
text-align: center; text-align: center;

View File

@ -1,21 +1,22 @@
import type { Topic } from '../../graphql/schema/core.gen' import type { 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, createEffect, createMemo, createSignal } from 'solid-js' import { For, Show, createEffect, createMemo, createSignal } from 'solid-js'
import { useFollowing } from '../../context/following' import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../../context/localize'
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'
import { dummyFilter } from '../../utils/dummyFilter' import { dummyFilter } from '../../../utils/dummyFilter'
import { getImageUrl } from '../../utils/getImageUrl' import { getImageUrl } from '../../../utils/getImageUrl'
import { scrollHandler } from '../../utils/scroll' import { scrollHandler } from '../../../utils/scroll'
import { TopicCard } from '../Topic/Card' import { TopicCard } from '../../Topic/Card'
import { Loading } from '../_shared/Loading' import { Loading } from '../../_shared/Loading'
import { SearchField } from '../_shared/SearchField' import { SearchField } from '../../_shared/SearchField'
import { TopicBadge } from '../../Topic/TopicBadge'
import styles from './AllTopics.module.scss' import styles from './AllTopics.module.scss'
type AllTopicsPageSearchParams = { type AllTopicsPageSearchParams = {
@ -29,7 +30,7 @@ type Props = {
const PAGE_SIZE = 20 const PAGE_SIZE = 20
export const AllTopicsView = (props: Props) => { export const AllTopics = (props: Props) => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const { searchParams, changeSearchParams } = useRouter<AllTopicsPageSearchParams>() const { searchParams, changeSearchParams } = useRouter<AllTopicsPageSearchParams>()
const [limit, setLimit] = createSignal(PAGE_SIZE) const [limit, setLimit] = createSignal(PAGE_SIZE)
@ -41,8 +42,6 @@ export const AllTopicsView = (props: Props) => {
sortBy: searchParams().by || 'shouts', sortBy: searchParams().by || 'shouts',
}) })
const { subscriptions } = useFollowing()
createEffect(() => { createEffect(() => {
if (!searchParams().by) { if (!searchParams().by) {
changeSearchParams({ changeSearchParams({
@ -76,7 +75,7 @@ export const AllTopicsView = (props: Props) => {
return keys return keys
}) })
const subscribed = (topicSlug: string) => subscriptions.topics.some((topic) => topic.slug === topicSlug) const { isOwnerSubscribed } = useFollowing()
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE) const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
const [searchQuery, setSearchQuery] = createSignal('') const [searchQuery, setSearchQuery] = createSignal('')
@ -186,28 +185,18 @@ export const AllTopicsView = (props: Props) => {
<Show when={searchParams().by && searchParams().by !== 'title'}> <Show when={searchParams().by && searchParams().by !== 'title'}>
<div class="row"> <div class="row">
<div class="col-lg-20 col-xl-18"> <div class="col-lg-20 col-xl-18 py-4">
<For each={filteredResults().slice(0, limit())}> <For each={filteredResults().slice(0, limit())}>
{(topic) => ( {(topic) => (
<> <>
<TopicCard <TopicBadge
topic={topic} topic={topic}
compact={false} isFollowed={{
subscribed={subscribed(topic.slug)} loaded: filteredResults().length > 0,
showPublications={true} value: isOwnerSubscribed(topic.slug),
showDescription={true} }}
showStat={true}
/> />
<div class={styles.stats}>
<span class={styles.statsItem}>
{t('shoutsWithCount', { count: topic.stat.shouts })}
</span>
<span class={styles.statsItem}>
{t('authorsWithCount', { count: topic.stat.authors })}
</span>
<span class={styles.statsItem}>
{t('followersWithCount', { count: topic.stat.followers })}
</span>
</div>
</> </>
)} )}
</For> </For>

View File

@ -0,0 +1 @@
export { AllTopics } from './AllTopics'

View File

@ -128,7 +128,6 @@ export const AuthorView = (props: Props) => {
const data = await apiClient.getReactionsBy({ const data = await apiClient.getReactionsBy({
by: { comment: false, created_by: commenter.id }, by: { comment: false, created_by: commenter.id },
}) })
console.debug('[components.Author] fetched comments', data)
setCommented(data) setCommented(data)
} }

View File

@ -20,7 +20,7 @@ interface FollowingContextType {
loadSubscriptions: () => void loadSubscriptions: () => void
follow: (what: FollowingEntity, slug: string) => Promise<void> follow: (what: FollowingEntity, slug: string) => Promise<void>
unfollow: (what: FollowingEntity, slug: string) => Promise<void> unfollow: (what: FollowingEntity, slug: string) => Promise<void>
isOwnerSubscribed: (userId: number) => boolean isOwnerSubscribed: (id: number | string) => boolean
} }
const FollowingContext = createContext<FollowingContextType>() const FollowingContext = createContext<FollowingContextType>()
@ -109,9 +109,11 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
} }
} }
const isOwnerSubscribed = (userId: number) => { const isOwnerSubscribed = (id?: number | string) => {
if (!author()) return if (!author() || !subscriptions) return
return !!subscriptions?.authors?.some((authorEntity) => authorEntity.id === userId) const isAuthorSubscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === id)
const isTopicSubscribed = subscriptions.topics?.some((topicEntity) => topicEntity.slug === id)
return !!isAuthorSubscribed || !!isTopicSubscribed
} }
const value: FollowingContextType = { const value: FollowingContextType = {

View File

@ -2,7 +2,7 @@ import type { PageProps } from './types'
import { createSignal, onMount } from 'solid-js' import { createSignal, onMount } from 'solid-js'
import { AllTopicsView } from '../components/Views/AllTopics' import { AllTopics } from '../components/Views/AllTopics'
import { PageLayout } from '../components/_shared/PageLayout' import { PageLayout } from '../components/_shared/PageLayout'
import { useLocalize } from '../context/localize' import { useLocalize } from '../context/localize'
import { loadAllTopics } from '../stores/zine/topics' import { loadAllTopics } from '../stores/zine/topics'
@ -23,7 +23,7 @@ export const AllTopicsPage = (props: PageProps) => {
return ( return (
<PageLayout title={t('Themes and plots')}> <PageLayout title={t('Themes and plots')}>
<AllTopicsView isLoaded={isLoaded()} topics={props.allTopics} /> <AllTopics isLoaded={isLoaded()} topics={props.allTopics} />
</PageLayout> </PageLayout>
) )
} }

View File

@ -50,4 +50,9 @@ export type UploadedFile = {
originalFilename?: string originalFilename?: string
} }
export type FollowedInfo = {
value?: boolean
loaded?: boolean
}
export type SubscriptionFilter = 'all' | 'authors' | 'topics' | 'communities' export type SubscriptionFilter = 'all' | 'authors' | 'topics' | 'communities'