Merge pull request #427 from Discours/hotfix/correct-following-status

Hotfix/correct following status
This commit is contained in:
Tony 2024-04-25 19:36:32 +03:00 committed by GitHub
commit b9f7d01339
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 236 additions and 219 deletions

View File

@ -528,5 +528,7 @@
"yesterday": "yesterday", "yesterday": "yesterday",
"Failed to delete comment": "Failed to delete comment", "Failed to delete comment": "Failed to delete comment",
"It's OK. Just enter your email to receive a link to change your password": "It's OK. Just enter your email to receive a link to change your password", "It's OK. Just enter your email to receive a link to change your password": "It's OK. Just enter your email to receive a link to change your password",
"Restore password": "Restore password" "Restore password": "Restore password",
"Subscribing...": "Subscribing...",
"Unsubscribing...": "Unsubscribing..."
} }

View File

@ -555,5 +555,7 @@
"yesterday": "вчера", "yesterday": "вчера",
"Failed to delete comment": "Не удалось удалить комментарий", "Failed to delete comment": "Не удалось удалить комментарий",
"It's OK. Just enter your email to receive a link to change your password": "Ничего страшного. Просто укажите свою почту, чтобы получить ссылку для смены пароля", "It's OK. Just enter your email to receive a link to change your password": "Ничего страшного. Просто укажите свою почту, чтобы получить ссылку для смены пароля",
"Restore password": "Восстановить пароль" "Restore password": "Восстановить пароль",
"Subscribing...": "Подписываем...",
"Unsubscribing...": "Отписываем..."
} }

View File

@ -40,6 +40,7 @@ import { InboxPage } from '../pages/inbox.page'
import { HomePage } from '../pages/index.page' import { HomePage } from '../pages/index.page'
import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page' import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
import { ProfileSettingsPage } from '../pages/profile/profileSettings.page' import { ProfileSettingsPage } from '../pages/profile/profileSettings.page'
//TODO: ProfileSubscriptionsPage - garbage code?
import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page' import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page'
import { SearchPage } from '../pages/search.page' import { SearchPage } from '../pages/search.page'
import { TopicPage } from '../pages/topic.page' import { TopicPage } from '../pages/topic.page'

View File

@ -115,8 +115,4 @@
} }
} }
} }
.actionButtonLabelHovered {
display: none;
}
} }

View File

@ -10,14 +10,12 @@ import { Author, FollowingEntity } from '../../../graphql/schema/core.gen'
import { router, useRouter } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
import { translit } from '../../../utils/ru2en' import { translit } from '../../../utils/ru2en'
import { isCyrillic } from '../../../utils/translate' import { isCyrillic } from '../../../utils/translate'
import { BadgeSubscribeButton } from '../../_shared/BadgeSubscribeButton'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
import { CheckButton } from '../../_shared/CheckButton' import { CheckButton } from '../../_shared/CheckButton'
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper' 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 styles from './AuthorBadge.module.scss' import styles from './AuthorBadge.module.scss'
type Props = { type Props = {
@ -29,13 +27,19 @@ type Props = {
inviteView?: boolean inviteView?: boolean
onInvite?: (id: number) => void onInvite?: (id: number) => void
selected?: boolean selected?: boolean
isFollowed?: FollowedInfo
} }
export const AuthorBadge = (props: Props) => { export const AuthorBadge = (props: Props) => {
const { mediaMatches } = useMediaQuery() const { mediaMatches } = useMediaQuery()
const { author, requireAuthentication } = useSession() const { author, requireAuthentication } = useSession()
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
const [isMobileView, setIsMobileView] = createSignal(false) const [isMobileView, setIsMobileView] = createSignal(false)
const [isFollowed, setIsFollowed] = createSignal<boolean>() const [isSubscribed, setIsSubscribed] = createSignal<boolean>()
createEffect(() => {
if (!subscriptions || !props.author) return
const subscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === props.author?.id)
setIsSubscribed(subscribed)
})
createEffect(() => { createEffect(() => {
setIsMobileView(!mediaMatches.sm) setIsMobileView(!mediaMatches.sm)
@ -67,20 +71,11 @@ export const AuthorBadge = (props: Props) => {
return props.author.name return props.author.name
}) })
createEffect(
on(
() => props.isFollowed,
() => {
setIsFollowed(props.isFollowed?.value)
},
),
)
const handleFollowClick = () => { const handleFollowClick = () => {
const value = !isFollowed()
requireAuthentication(() => { requireAuthentication(() => {
setIsFollowed(value) isSubscribed()
setFollowing(FollowingEntity.Author, props.author.slug, value) ? unfollow(FollowingEntity.Author, props.author.slug)
: follow(FollowingEntity.Author, props.author.slug)
}, 'subscribe') }, 'subscribe')
} }
@ -134,55 +129,13 @@ export const AuthorBadge = (props: Props) => {
</div> </div>
<Show when={props.author.slug !== author()?.slug && !props.nameOnly}> <Show when={props.author.slug !== author()?.slug && !props.nameOnly}>
<div class={styles.actions}> <div class={styles.actions}>
<Show <BadgeSubscribeButton
when={!props.minimizeSubscribeButton} action={() => handleFollowClick()}
fallback={<CheckButton text={t('Follow')} checked={isFollowed()} onClick={handleFollowClick} />} isSubscribed={isSubscribed()}
> actionMessageType={
<Show subscribeInAction()?.slug === props.author.slug ? subscribeInAction().type : undefined
when={isFollowed()} }
fallback={ />
<Button
variant={props.iconButtons ? 'secondary' : 'bordered'}
size="S"
value={
<Show when={props.iconButtons} fallback={t('Subscribe')}>
<Icon name="author-subscribe" class={stylesButton.icon} />
</Show>
}
onClick={handleFollowClick}
isSubscribeButton={true}
class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: isFollowed(),
})}
/>
}
>
<Button
variant={props.iconButtons ? 'secondary' : 'bordered'}
size="S"
value={
<Show
when={props.iconButtons}
fallback={
<>
<span class={styles.actionButtonLabel}>{t('Following')}</span>
<span class={styles.actionButtonLabelHovered}>{t('Unfollow')}</span>
</>
}
>
<Icon name="author-unsubscribe" class={stylesButton.icon} />
</Show>
}
onClick={handleFollowClick}
isSubscribeButton={true}
class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: isFollowed(),
})}
/>
</Show>
</Show>
<Show when={props.showMessageButton}> <Show when={props.showMessageButton}>
<Button <Button
variant={props.iconButtons ? 'secondary' : 'bordered'} variant={props.iconButtons ? 'secondary' : 'bordered'}

View File

@ -34,16 +34,18 @@ export const AuthorCard = (props: Props) => {
const { author, isSessionLoaded, requireAuthentication } = useSession() const { author, isSessionLoaded, requireAuthentication } = useSession()
const [authorSubs, setAuthorSubs] = createSignal<Array<Author | Topic | Community>>([]) const [authorSubs, setAuthorSubs] = createSignal<Array<Author | Topic | Community>>([])
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all') const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
const [isFollowed, setIsFollowed] = createSignal<boolean>() const [isSubscribed, setIsSubscribed] = createSignal<boolean>()
const isProfileOwner = createMemo(() => author()?.slug === props.author.slug) const isProfileOwner = createMemo(() => author()?.slug === props.author.slug)
const { setFollowing, isOwnerSubscribed } = useFollowing() const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
onMount(() => { onMount(() => {
setAuthorSubs(props.following) setAuthorSubs(props.following)
}) })
createEffect(() => { createEffect(() => {
setIsFollowed(isOwnerSubscribed(props.author?.id)) if (!subscriptions || !props.author) return
const subscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === props.author?.id)
setIsSubscribed(subscribed)
}) })
const name = createMemo(() => { const name = createMemo(() => {
@ -83,15 +85,19 @@ export const AuthorCard = (props: Props) => {
}) })
const handleFollowClick = () => { const handleFollowClick = () => {
const value = !isFollowed()
requireAuthentication(() => { requireAuthentication(() => {
setIsFollowed(value) isSubscribed()
setFollowing(FollowingEntity.Author, props.author.slug, value) ? unfollow(FollowingEntity.Author, props.author.slug)
: follow(FollowingEntity.Author, props.author.slug)
}, 'subscribe') }, 'subscribe')
} }
const followButtonText = createMemo(() => { const followButtonText = createMemo(() => {
if (isOwnerSubscribed(props.author?.id)) { if (subscribeInAction()?.slug === props.author.slug) {
return subscribeInAction().type === 'subscribe' ? t('Subscribing...') : t('Unsubscribing...')
}
if (isSubscribed()) {
return ( return (
<> <>
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span> <span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
@ -119,12 +125,7 @@ export const AuthorCard = (props: Props) => {
<Show when={props.author.bio}> <Show when={props.author.bio}>
<div class={styles.authorAbout} innerHTML={props.author.bio} /> <div class={styles.authorAbout} innerHTML={props.author.bio} />
</Show> </Show>
<Show <Show when={props.followers?.length > 0 || props.following?.length > 0}>
when={
(props.followers && props.followers.length > 0) ||
(props.following && props.following.length > 0)
}
>
<div class={styles.subscribersContainer}> <div class={styles.subscribersContainer}>
<Show when={props.followers && props.followers.length > 0}> <Show when={props.followers && props.followers.length > 0}>
<a href="?m=followers" class={styles.subscribers}> <a href="?m=followers" class={styles.subscribers}>
@ -204,13 +205,14 @@ export const AuthorCard = (props: Props) => {
when={isProfileOwner()} when={isProfileOwner()}
fallback={ fallback={
<div class={styles.authorActions}> <div class={styles.authorActions}>
<Show when={authorSubs().length}> <Show when={authorSubs()?.length}>
<Button <Button
onClick={handleFollowClick} onClick={handleFollowClick}
disabled={Boolean(subscribeInAction())}
value={followButtonText()} value={followButtonText()}
isSubscribeButton={true} isSubscribeButton={true}
class={clsx({ class={clsx({
[stylesButton.subscribed]: isFollowed(), [stylesButton.subscribed]: isSubscribed(),
})} })}
/> />
</Show> </Show>
@ -255,15 +257,7 @@ export const AuthorCard = (props: Props) => {
<div class="row"> <div class="row">
<div class="col-24"> <div class="col-24">
<For each={props.followers}> <For each={props.followers}>
{(follower: Author) => ( {(follower: Author) => <AuthorBadge author={follower} />}
<AuthorBadge
author={follower}
isFollowed={{
loaded: Boolean(authorSubs()),
value: isOwnerSubscribed(follower.id),
}}
/>
)}
</For> </For>
</div> </div>
</div> </div>
@ -318,21 +312,9 @@ export const AuthorCard = (props: Props) => {
<For each={authorSubs()}> <For each={authorSubs()}>
{(subscription) => {(subscription) =>
isAuthor(subscription) ? ( isAuthor(subscription) ? (
<AuthorBadge <AuthorBadge author={subscription} />
isFollowed={{
loaded: Boolean(authorSubs()),
value: isOwnerSubscribed(subscription.id),
}}
author={subscription}
/>
) : ( ) : (
<TopicBadge <TopicBadge topic={subscription} />
isFollowed={{
loaded: Boolean(authorSubs()),
value: isOwnerSubscribed(subscription.id),
}}
topic={subscription}
/>
) )
} }
</For> </For>

View File

@ -21,7 +21,6 @@ const PAGE_SIZE = 20
export const AuthorsList = (props: Props) => { export const AuthorsList = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { isOwnerSubscribed } = useFollowing()
const { authorsByShouts, authorsByFollowers } = useAuthorsStore() const { authorsByShouts, authorsByFollowers } = useAuthorsStore()
const [loading, setLoading] = createSignal(false) const [loading, setLoading] = createSignal(false)
const [currentPage, setCurrentPage] = createSignal({ shouts: 0, followers: 0 }) const [currentPage, setCurrentPage] = createSignal({ shouts: 0, followers: 0 })
@ -83,13 +82,7 @@ export const AuthorsList = (props: Props) => {
{(author) => ( {(author) => (
<div class="row"> <div class="row">
<div class="col-lg-20 col-xl-18"> <div class="col-lg-20 col-xl-18">
<AuthorBadge <AuthorBadge author={author} />
author={author}
isFollowed={{
loaded: !loading(),
value: isOwnerSubscribed(author.id),
}}
/>
</div> </div>
</div> </div>
)} )}

View File

@ -30,7 +30,6 @@ type Props = {
export const Beside = (props: Props) => { export const Beside = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { isOwnerSubscribed } = useFollowing()
return ( return (
<Show when={!!props.beside?.slug && props.values?.length > 0}> <Show when={!!props.beside?.slug && props.values?.length > 0}>
@ -86,12 +85,7 @@ export const Beside = (props: Props) => {
/> />
</Show> </Show>
<Show when={props.wrapper === 'author'}> <Show when={props.wrapper === 'author'}>
<AuthorBadge <AuthorBadge author={value as Author} />
author={value as Author}
isFollowed={{
value: isOwnerSubscribed(value.id),
}}
/>
</Show> </Show>
<Show when={props.wrapper === 'article' && value?.slug}> <Show when={props.wrapper === 'article' && value?.slug}>
<ArticleCard <ArticleCard

View File

@ -1,5 +1,5 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createMemo, createSignal } from 'solid-js' import { 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'
@ -38,14 +38,18 @@ export const TopicCard = (props: TopicProps) => {
capitalize(lang() === 'en' ? props.topic.slug.replaceAll('-', ' ') : props.topic.title || ''), capitalize(lang() === 'en' ? props.topic.slug.replaceAll('-', ' ') : props.topic.title || ''),
) )
const { author, requireAuthentication } = useSession() const { author, requireAuthentication } = useSession()
const { setFollowing, loading: subLoading } = useFollowing() const [isSubscribed, setIsSubscribed] = createSignal()
const [followed, setFollowed] = createSignal() const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
createEffect(() => {
if (!subscriptions || !props.topic) return
const subscribed = subscriptions.topics?.some((topics) => topics.id === props.topic?.id)
setIsSubscribed(subscribed)
})
const handleFollowClick = () => { const handleFollowClick = () => {
const value = !followed()
requireAuthentication(() => { requireAuthentication(() => {
setFollowed(value) follow(FollowingEntity.Topic, props.topic.slug)
setFollowing(FollowingEntity.Topic, props.topic.slug, value)
}, 'subscribe') }, 'subscribe')
} }
@ -53,12 +57,12 @@ export const TopicCard = (props: TopicProps) => {
return ( return (
<> <>
<Show when={props.iconButton}> <Show when={props.iconButton}>
<Show when={followed()} fallback="+"> <Show when={isSubscribed()} fallback="+">
<Icon name="check-subscribed" /> <Icon name="check-subscribed" />
</Show> </Show>
</Show> </Show>
<Show when={!props.iconButton}> <Show when={!props.iconButton}>
<Show when={followed()} fallback={t('Follow')}> <Show when={isSubscribed()} 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>
@ -130,7 +134,7 @@ export const TopicCard = (props: TopicProps) => {
fallback={ fallback={
<CheckButton <CheckButton
text={t('Follow')} text={t('Follow')}
checked={Boolean(followed())} checked={Boolean(isSubscribed())}
onClick={handleFollowClick} onClick={handleFollowClick}
/> />
} }
@ -142,10 +146,10 @@ export const TopicCard = (props: TopicProps) => {
onClick={handleFollowClick} onClick={handleFollowClick}
isSubscribeButton={true} isSubscribeButton={true}
class={clsx(styles.actionButton, { class={clsx(styles.actionButton, {
[styles.isSubscribing]: subLoading(), [styles.isSubscribing]:
[stylesButton.subscribed]: followed(), subscribeInAction()?.slug === props.topic.slug ? subscribeInAction().type : undefined,
[stylesButton.subscribed]: isSubscribed(),
})} })}
// disabled={subLoading()}
/> />
</Show> </Show>
</Show> </Show>

View File

@ -8,16 +8,12 @@ import { useSession } from '../../../context/session'
import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen' import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
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 { BadgeSubscribeButton } from '../../_shared/BadgeSubscribeButton'
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 showStat?: boolean
} }
@ -26,14 +22,20 @@ export const TopicBadge = (props: Props) => {
const { mediaMatches } = useMediaQuery() const { mediaMatches } = useMediaQuery()
const [isMobileView, setIsMobileView] = createSignal(false) const [isMobileView, setIsMobileView] = createSignal(false)
const { requireAuthentication } = useSession() const { requireAuthentication } = useSession()
const { setFollowing, loading: subLoading } = useFollowing() const [isSubscribed, setIsSubscribed] = createSignal<boolean>()
const [isFollowed, setIsFollowed] = createSignal<boolean>() const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
createEffect(() => {
if (!subscriptions || !props.topic) return
const subscribed = subscriptions.topics?.some((topics) => topics.id === props.topic?.id)
setIsSubscribed(subscribed)
})
const handleFollowClick = () => { const handleFollowClick = () => {
const value = !isFollowed()
requireAuthentication(() => { requireAuthentication(() => {
setIsFollowed(value) isSubscribed()
setFollowing(FollowingEntity.Topic, props.topic.slug, value) ? follow(FollowingEntity.Topic, props.topic.slug)
: unfollow(FollowingEntity.Topic, props.topic.slug)
}, 'subscribe') }, 'subscribe')
} }
@ -41,15 +43,6 @@ 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
@ -83,35 +76,14 @@ export const TopicBadge = (props: Props) => {
</Show> </Show>
</a> </a>
</div> </div>
<div class={styles.actions}> <div class={styles.actions}>
<Show <BadgeSubscribeButton
when={!props.minimizeSubscribeButton} isSubscribed={isSubscribed()}
fallback={ action={handleFollowClick}
<CheckButton text={t('Follow')} checked={Boolean(isFollowed())} onClick={handleFollowClick} /> actionMessageType={
subscribeInAction()?.slug === props.topic.slug ? subscribeInAction().type : undefined
} }
> />
<Show
when={isFollowed()}
fallback={
<Button
variant="primary"
size="S"
value={subLoading() ? t('subscribing...') : t('Subscribe')}
onClick={handleFollowClick}
class={styles.subscribeButton}
/>
}
>
<Button
onClick={handleFollowClick}
variant="bordered"
size="S"
value={t('Following')}
class={styles.subscribeButton}
/>
</Show>
</Show>
</div> </div>
</div> </div>
<div class={styles.stats}> <div class={styles.stats}>

View File

@ -74,8 +74,6 @@ export const AllTopics = (props: Props) => {
return keys return keys
}) })
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('')
const filteredResults = createMemo(() => { const filteredResults = createMemo(() => {
@ -188,14 +186,7 @@ export const AllTopics = (props: Props) => {
<For each={filteredResults().slice(0, limit())}> <For each={filteredResults().slice(0, limit())}>
{(topic) => ( {(topic) => (
<> <>
<TopicBadge <TopicBadge topic={topic} showStat={true} />
topic={topic}
isFollowed={{
loaded: filteredResults().length > 0,
value: isOwnerSubscribed(topic.slug),
}}
showStat={true}
/>
</> </>
)} )}
</For> </For>

View File

@ -3,7 +3,7 @@ import type { Author, Reaction, Shout, Topic } from '../../../graphql/schema/cor
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { Meta, Title } from '@solidjs/meta' import { Meta, Title } from '@solidjs/meta'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, onMount } from 'solid-js' import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
import { useFollowing } from '../../../context/following' import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
@ -75,7 +75,7 @@ export const AuthorView = (props: Props) => {
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 fetchData = async (slug) => { const fetchData = async (slug: string) => {
try { try {
const [subscriptionsResult, followersResult, authorResult] = await Promise.all([ const [subscriptionsResult, followersResult, authorResult] = await Promise.all([
apiClient.getAuthorFollows({ slug }), apiClient.getAuthorFollows({ slug }),
@ -118,7 +118,6 @@ export const AuthorView = (props: Props) => {
// pagination // pagination
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) { if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
loadMore() loadMore()
loadSubscriptions()
} }
}) })
@ -135,6 +134,7 @@ export const AuthorView = (props: Props) => {
createEffect(() => { createEffect(() => {
if (author()) { if (author()) {
fetchData(author().slug)
fetchComments(author()) fetchComments(author())
} }
}) })

View File

@ -0,0 +1,29 @@
.actionButton {
border-radius: 0.8rem !important;
margin-right: 0 !important;
width: 9em;
&.iconed {
padding: 6px !important;
min-width: 4rem;
width: unset;
&:hover img {
filter: invert(1);
}
}
&:hover {
.actionButtonLabel {
display: none;
}
.actionButtonLabelHovered {
display: block;
}
}
}
.actionButtonLabelHovered {
display: none;
}

View File

@ -0,0 +1,87 @@
import { clsx } from 'clsx'
import { Show, createMemo } from 'solid-js'
import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize'
import { Button } from '../Button'
import stylesButton from '../Button/Button.module.scss'
import { CheckButton } from '../CheckButton'
import { Icon } from '../Icon'
import styles from './BadgeDubscribeButton.module.scss'
type Props = {
class?: string
isSubscribed: boolean
minimizeSubscribeButton?: boolean
action: () => void
iconButtons?: boolean
actionMessageType?: 'subscribe' | 'unsubscribe'
}
export const BadgeSubscribeButton = (props: Props) => {
const { t } = useLocalize()
const inActionText = createMemo(() => {
return props.actionMessageType === 'subscribe' ? t('Subscribing...') : t('Unsubscribing...')
})
return (
<div class={props.class}>
<Show
when={!props.minimizeSubscribeButton}
fallback={<CheckButton text={t('Follow')} checked={props.isSubscribed} onClick={props.action} />}
>
<Show
when={props.isSubscribed}
fallback={
<Button
variant={props.iconButtons ? 'secondary' : 'bordered'}
size="S"
value={
<Show
when={props.iconButtons}
fallback={props.actionMessageType ? inActionText() : t('Subscribe')}
>
<Icon name="author-subscribe" class={stylesButton.icon} />
</Show>
}
onClick={props.action}
isSubscribeButton={true}
class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: props.isSubscribed,
})}
/>
}
>
<Button
variant={props.iconButtons ? 'secondary' : 'bordered'}
size="S"
value={
<Show
when={props.iconButtons}
fallback={
props.actionMessageType ? (
inActionText()
) : (
<>
<span class={styles.actionButtonLabel}>{t('Following')}</span>
<span class={styles.actionButtonLabelHovered}>{t('Unfollow')}</span>
</>
)
}
>
<Icon name="author-unsubscribe" class={stylesButton.icon} />
</Show>
}
onClick={props.action}
isSubscribeButton={true}
class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: props.isSubscribed,
})}
/>
</Show>
</Show>
</div>
)
}

View File

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

View File

@ -1,11 +1,19 @@
import { Accessor, JSX, createContext, createEffect, createSignal, useContext } from 'solid-js' import { Accessor, JSX, createContext, createEffect, createMemo, createSignal, useContext } from 'solid-js'
import { createStore } from 'solid-js/store' import { createStore } from 'solid-js/store'
import { apiClient } from '../graphql/client/core' import { apiClient } from '../graphql/client/core'
import { Author, AuthorFollowsResult, FollowingEntity } from '../graphql/schema/core.gen' import { Author, AuthorFollowsResult, Community, FollowingEntity, Topic } from '../graphql/schema/core.gen'
import { useSession } from './session' import { useSession } from './session'
export type SubscriptionsData = {
topics?: Topic[]
authors?: Author[]
communities?: Community[]
}
type SubscribeAction = { slug: string; type: 'subscribe' | 'unsubscribe' }
interface FollowingContextType { interface FollowingContextType {
loading: Accessor<boolean> loading: Accessor<boolean>
followers: Accessor<Array<Author>> followers: Accessor<Array<Author>>
@ -15,7 +23,8 @@ 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: (id: number | string) => boolean // followers: Accessor<Author[]>
subscribeInAction?: Accessor<SubscribeAction>
} }
const FollowingContext = createContext<FollowingContextType>() const FollowingContext = createContext<FollowingContextType>()
@ -43,7 +52,6 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
console.debug('[context.following] fetching subs data...') console.debug('[context.following] fetching subs data...')
const result = await apiClient.getAuthorFollows({ user: session()?.user.id }) const result = await apiClient.getAuthorFollows({ user: session()?.user.id })
setSubscriptions(result || EMPTY_SUBSCRIPTIONS) setSubscriptions(result || EMPTY_SUBSCRIPTIONS)
console.info('[context.following] subs:', subscriptions)
} }
} catch (error) { } catch (error) {
console.info('[context.following] cannot get subs', error) console.info('[context.following] cannot get subs', error)
@ -52,28 +60,37 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
} }
} }
createEffect(() => {
console.info('[context.following] subs:', subscriptions)
})
const [subscribeInAction, setSubscribeInAction] = createSignal<SubscribeAction>()
const follow = async (what: FollowingEntity, slug: string) => { const follow = async (what: FollowingEntity, slug: string) => {
if (!author()) return if (!author()) return
setSubscribeInAction({ slug, type: 'subscribe' })
try { try {
await apiClient.follow({ what, slug }) const subscriptionData = await apiClient.follow({ what, slug })
setSubscriptions((prevSubscriptions) => { setSubscriptions((prevSubscriptions) => {
const updatedSubs = { ...prevSubscriptions } if (!prevSubscriptions[what]) prevSubscriptions[what] = []
if (!updatedSubs[what]) updatedSubs[what] = [] prevSubscriptions[what].push(subscriptionData)
const exists = updatedSubs[what]?.some((entity) => entity.slug === slug) return prevSubscriptions
if (!exists) updatedSubs[what].push(slug)
return updatedSubs
}) })
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} finally {
setSubscribeInAction() // Сбрасываем состояние действия подписки.
} }
} }
const unfollow = async (what: FollowingEntity, slug: string) => { const unfollow = async (what: FollowingEntity, slug: string) => {
if (!author()) return if (!author()) return
setSubscribeInAction({ slug: slug, type: 'unsubscribe' })
try { try {
await apiClient.unfollow({ what, slug }) await apiClient.unfollow({ what, slug })
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} finally {
setSubscribeInAction()
} }
} }
@ -114,23 +131,17 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
} }
} }
const isOwnerSubscribed = (id?: number | string) => {
if (!author() || !subscriptions) return
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 = {
loading, loading,
subscriptions, subscriptions,
setSubscriptions, setSubscriptions,
isOwnerSubscribed,
setFollowing, setFollowing,
followers, followers,
loadSubscriptions: fetchData, loadSubscriptions: fetchData,
follow, follow,
unfollow, unfollow,
// followers,
subscribeInAction,
} }
return <FollowingContext.Provider value={value}>{props.children}</FollowingContext.Provider> return <FollowingContext.Provider value={value}>{props.children}</FollowingContext.Provider>

View File

@ -4,6 +4,10 @@ export default gql`
mutation FollowMutation($what: FollowingEntity!, $slug: String!) { mutation FollowMutation($what: FollowingEntity!, $slug: String!) {
follow(what: $what, slug: $slug) { follow(what: $what, slug: $slug) {
error error
authors {
id
slug
}
} }
} }
` `

View File

@ -53,9 +53,4 @@ 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'