refactoring:following

This commit is contained in:
Untone 2024-05-20 14:16:54 +03:00
parent a25d50d99b
commit 652d0b647a
24 changed files with 290 additions and 236 deletions

View File

@ -10,17 +10,17 @@ 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 { FollowingButton } from '../../_shared/FollowingButton'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { Userpic } from '../Userpic' import { Userpic } from '../Userpic'
import styles from './AuthorBadge.module.scss' import styles from './AuthorBadge.module.scss'
type Props = { type Props = {
author: Author author: Author
minimizeSubscribeButton?: boolean minimize?: boolean
showMessageButton?: boolean showMessageButton?: boolean
iconButtons?: boolean iconButtons?: boolean
nameOnly?: boolean nameOnly?: boolean
@ -32,14 +32,14 @@ type Props = {
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 { follow, unfollow, follows, following } = useFollowing()
const [isMobileView, setIsMobileView] = createSignal(false) const [isMobileView, setIsMobileView] = createSignal(false)
const [isSubscribed, setIsSubscribed] = createSignal<boolean>() const [isFollowed, setIsFollowed] = createSignal<boolean>()
createEffect(() => { createEffect(() => {
if (!(subscriptions && props.author)) return if (!(follows && props.author)) return
const subscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === props.author?.id) const followed = follows?.authors?.some((authorEntity) => authorEntity.id === props.author?.id)
setIsSubscribed(subscribed) setIsFollowed(followed)
}) })
createEffect(() => { createEffect(() => {
@ -73,9 +73,9 @@ export const AuthorBadge = (props: Props) => {
const handleFollowClick = () => { const handleFollowClick = () => {
requireAuthentication(async () => { requireAuthentication(async () => {
const handle = isSubscribed() ? unfollow : follow const handle = isFollowed() ? unfollow : follow
await handle(FollowingEntity.Author, props.author.slug) await handle(FollowingEntity.Author, props.author.slug)
}, 'subscribe') }, 'follow')
} }
return ( return (
@ -131,12 +131,10 @@ 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}>
<BadgeSubscribeButton <FollowingButton
action={() => handleFollowClick()} action={() => handleFollowClick()}
isSubscribed={isSubscribed()} isFollowed={isFollowed()}
actionMessageType={ actionMessageType={following()?.slug === props.author.slug ? following().type : undefined}
subscribeInAction()?.slug === props.author.slug ? subscribeInAction().type : undefined
}
/> />
<Show when={props.showMessageButton}> <Show when={props.showMessageButton}>
<Button <Button

View File

@ -8,7 +8,7 @@ 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 { FollowsFilter } from '../../../pages/types'
import { router, useRouter } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
import { isAuthor } from '../../../utils/isAuthor' import { isAuthor } from '../../../utils/isAuthor'
import { translit } from '../../../utils/ru2en' import { translit } from '../../../utils/ru2en'
@ -33,19 +33,19 @@ export const AuthorCard = (props: Props) => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
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 [followsFilter, setFollowsFilter] = createSignal<FollowsFilter>('all')
const [isSubscribed, setIsSubscribed] = createSignal<boolean>() const [isFollowed, setIsFollowed] = createSignal<boolean>()
const isProfileOwner = createMemo(() => author()?.slug === props.author.slug) const isProfileOwner = createMemo(() => author()?.slug === props.author.slug)
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing() const { follow, unfollow, follows, following } = useFollowing()
onMount(() => { onMount(() => {
setAuthorSubs(props.following) setAuthorSubs(props.following)
}) })
createEffect(() => { createEffect(() => {
if (!(subscriptions && props.author)) return if (!(follows && props.author)) return
const subscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === props.author?.id) const followed = follows?.authors?.some((authorEntity) => authorEntity.id === props.author?.id)
setIsSubscribed(subscribed) setIsFollowed(followed)
}) })
const name = createMemo(() => { const name = createMemo(() => {
@ -72,11 +72,11 @@ export const AuthorCard = (props: Props) => {
createEffect(() => { createEffect(() => {
if (props.following) { if (props.following) {
if (subscriptionFilter() === 'authors') { if (followsFilter() === 'authors') {
setAuthorSubs(props.following.filter((s) => 'name' in s)) setAuthorSubs(props.following.filter((s) => 'name' in s))
} else if (subscriptionFilter() === 'topics') { } else if (followsFilter() === 'topics') {
setAuthorSubs(props.following.filter((s) => 'title' in s)) setAuthorSubs(props.following.filter((s) => 'title' in s))
} else if (subscriptionFilter() === 'communities') { } else if (followsFilter() === 'communities') {
setAuthorSubs(props.following.filter((s) => 'title' in s)) setAuthorSubs(props.following.filter((s) => 'title' in s))
} else { } else {
setAuthorSubs(props.following) setAuthorSubs(props.following)
@ -86,18 +86,18 @@ export const AuthorCard = (props: Props) => {
const handleFollowClick = () => { const handleFollowClick = () => {
requireAuthentication(() => { requireAuthentication(() => {
isSubscribed() isFollowed()
? unfollow(FollowingEntity.Author, props.author.slug) ? unfollow(FollowingEntity.Author, props.author.slug)
: follow(FollowingEntity.Author, props.author.slug) : follow(FollowingEntity.Author, props.author.slug)
}, 'subscribe') }, 'follow')
} }
const followButtonText = createMemo(() => { const followButtonText = createMemo(() => {
if (subscribeInAction()?.slug === props.author.slug) { if (following()?.slug === props.author.slug) {
return subscribeInAction().type === 'subscribe' ? t('Subscribing...') : t('Unsubscribing...') return following().type === 'follow' ? t('Following...') : t('Unfollowing...')
} }
if (isSubscribed()) { if (isFollowed()) {
return ( return (
<> <>
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span> <span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
@ -208,11 +208,11 @@ export const AuthorCard = (props: Props) => {
<Show when={authorSubs()?.length}> <Show when={authorSubs()?.length}>
<Button <Button
onClick={handleFollowClick} onClick={handleFollowClick}
disabled={Boolean(subscribeInAction())} disabled={Boolean(following())}
value={followButtonText()} value={followButtonText()}
isSubscribeButton={true} isSubscribeButton={true}
class={clsx({ class={clsx({
[stylesButton.subscribed]: isSubscribed(), [stylesButton.followed]: isFollowed(),
})} })}
/> />
</Show> </Show>
@ -272,20 +272,20 @@ export const AuthorCard = (props: Props) => {
<ul class="view-switcher"> <ul class="view-switcher">
<li <li
class={clsx({ class={clsx({
'view-switcher__item--selected': subscriptionFilter() === 'all', 'view-switcher__item--selected': followsFilter() === 'all',
})} })}
> >
<button type="button" onClick={() => setSubscriptionFilter('all')}> <button type="button" onClick={() => setFollowsFilter('all')}>
{t('All')} {t('All')}
</button> </button>
<span class="view-switcher__counter">{props.following.length}</span> <span class="view-switcher__counter">{props.following.length}</span>
</li> </li>
<li <li
class={clsx({ class={clsx({
'view-switcher__item--selected': subscriptionFilter() === 'authors', 'view-switcher__item--selected': followsFilter() === 'authors',
})} })}
> >
<button type="button" onClick={() => setSubscriptionFilter('authors')}> <button type="button" onClick={() => setFollowsFilter('authors')}>
{t('Authors')} {t('Authors')}
</button> </button>
<span class="view-switcher__counter"> <span class="view-switcher__counter">
@ -294,10 +294,10 @@ export const AuthorCard = (props: Props) => {
</li> </li>
<li <li
class={clsx({ class={clsx({
'view-switcher__item--selected': subscriptionFilter() === 'topics', 'view-switcher__item--selected': followsFilter() === 'topics',
})} })}
> >
<button type="button" onClick={() => setSubscriptionFilter('topics')}> <button type="button" onClick={() => setFollowsFilter('topics')}>
{t('Topics')} {t('Topics')}
</button> </button>
<span class="view-switcher__counter"> <span class="view-switcher__counter">

View File

@ -30,7 +30,7 @@ export default Node.create({
addOptions() { addOptions() {
return { return {
'data-type': 'incut', 'data-type': 'incut',
}; }
}, },
addAttributes() { addAttributes() {

View File

@ -17,7 +17,7 @@ export const CustomBlockquote = Blockquote.extend({
content: 'block+', content: 'block+',
addOptions(): BlockquoteOptions { addOptions(): BlockquoteOptions {
return {} as BlockquoteOptions; return {} as BlockquoteOptions
}, },
addAttributes() { addAttributes() {
@ -35,15 +35,13 @@ export const CustomBlockquote = Blockquote.extend({
addCommands() { addCommands() {
return { return {
toggleBlockquote: toggleBlockquote:
(type) => ({ commands }) => commands.toggleWrap( (type) =>
this.name, ({ commands }) =>
{ 'data-type': type } commands.toggleWrap(this.name, { 'data-type': type }),
),
setBlockQuoteFloat: setBlockQuoteFloat:
(value) => ({ commands }) => commands.updateAttributes( (value) =>
this.name, ({ commands }) =>
{ 'data-float': value } commands.updateAttributes(this.name, { 'data-float': value }),
),
} }
}, },
}) })

View File

@ -15,7 +15,7 @@ import styles from './Sidebar.module.scss'
export const Sidebar = () => { export const Sidebar = () => {
const { t } = useLocalize() const { t } = useLocalize()
const { seen } = useSeen() const { seen } = useSeen()
const { subscriptions } = useFollowing() const { follows } = useFollowing()
const { page } = useRouter() const { page } = useRouter()
const { articlesByTopic, articlesByAuthor } = useArticlesStore() const { articlesByTopic, articlesByAuthor } = useArticlesStore()
const [isSubscriptionsVisible, setSubscriptionsVisible] = createSignal(true) const [isSubscriptionsVisible, setSubscriptionsVisible] = createSignal(true)
@ -111,7 +111,7 @@ export const Sidebar = () => {
</li> </li>
</ul> </ul>
<Show when={subscriptions.authors.length > 0 || subscriptions.topics.length > 0}> <Show when={follows?.authors?.length > 0 || follows?.topics?.length > 0}>
<h4 <h4
classList={{ [styles.opened]: isSubscriptionsVisible() }} classList={{ [styles.opened]: isSubscriptionsVisible() }}
onClick={() => { onClick={() => {
@ -123,7 +123,7 @@ 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={follows.authors}>
{(a: Author) => ( {(a: Author) => (
<li> <li>
<a href={`/author/${a.slug}`} classList={{ [styles.unread]: checkAuthorIsSeen(a.slug) }}> <a href={`/author/${a.slug}`} classList={{ [styles.unread]: checkAuthorIsSeen(a.slug) }}>
@ -135,7 +135,7 @@ export const Sidebar = () => {
</li> </li>
)} )}
</For> </For>
<For each={subscriptions.topics}> <For each={follows.topics}>
{(topic) => ( {(topic) => (
<li> <li>
<a <a

View File

@ -123,12 +123,12 @@
width: 9em; width: 9em;
} }
.isSubscribing { .isFollowing {
opacity: 0.5; opacity: 0.5;
} }
/* /*
.isSubscribed { .isFollowed {
background: #000; background: #000;
color: #fff; color: #fff;
transition: transition:

View File

@ -1,5 +1,5 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createEffect, createMemo, createSignal } from 'solid-js' import { Show, createEffect, createMemo, 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'
@ -18,7 +18,7 @@ import styles from './Card.module.scss'
interface TopicProps { interface TopicProps {
topic: Topic topic: Topic
compact?: boolean compact?: boolean
subscribed?: boolean followed?: boolean
shortDescription?: boolean shortDescription?: boolean
subscribeButtonBottom?: boolean subscribeButtonBottom?: boolean
additionalClass?: string additionalClass?: string
@ -27,7 +27,7 @@ interface TopicProps {
showPublications?: boolean showPublications?: boolean
showDescription?: boolean showDescription?: boolean
isCardMode?: boolean isCardMode?: boolean
minimizeSubscribeButton?: boolean minimize?: boolean
isNarrow?: boolean isNarrow?: boolean
withIcon?: boolean withIcon?: boolean
} }
@ -38,33 +38,40 @@ 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 [isSubscribed, setIsSubscribed] = createSignal() const [isFollowed, setIsFollowed] = createSignal()
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing() const { follow, unfollow, follows, following } = useFollowing()
createEffect(() => { createEffect(
if (!(subscriptions && props.topic)) return on(
const subscribed = subscriptions.topics?.some((topics) => topics.id === props.topic?.id) [() => follows, () => props.topic],
setIsSubscribed(subscribed) ([flws, tpc]) => {
}) if (flws && tpc) {
const followed = flws.topics?.some((topics) => topics.id === props.topic?.id)
setIsFollowed(followed)
}
},
{ defer: true },
),
)
const handleFollowClick = () => { const handleFollowClick = () => {
requireAuthentication(() => { requireAuthentication(() => {
isSubscribed() isFollowed()
? unfollow(FollowingEntity.Topic, props.topic.slug) ? unfollow(FollowingEntity.Topic, props.topic.slug)
: follow(FollowingEntity.Topic, props.topic.slug) : follow(FollowingEntity.Topic, props.topic.slug)
}, 'subscribe') }, 'follow')
} }
const subscribeValue = () => { const subscribeValue = () => {
return ( return (
<> <>
<Show when={props.iconButton}> <Show when={props.iconButton}>
<Show when={isSubscribed()} fallback="+"> <Show when={isFollowed()} fallback="+">
<Icon name="check-subscribed" /> <Icon name="check-followed" />
</Show> </Show>
</Show> </Show>
<Show when={!props.iconButton}> <Show when={!props.iconButton}>
<Show when={isSubscribed()} fallback={t('Follow')}> <Show when={isFollowed()} 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>
@ -132,11 +139,11 @@ export const TopicCard = (props: TopicProps) => {
<ShowOnlyOnClient> <ShowOnlyOnClient>
<Show when={author()}> <Show when={author()}>
<Show <Show
when={!props.minimizeSubscribeButton} when={!props.minimize}
fallback={ fallback={
<CheckButton <CheckButton
text={t('Follow')} text={t('Follow')}
checked={Boolean(isSubscribed())} checked={Boolean(isFollowed())}
onClick={handleFollowClick} onClick={handleFollowClick}
/> />
} }
@ -148,9 +155,9 @@ export const TopicCard = (props: TopicProps) => {
onClick={handleFollowClick} onClick={handleFollowClick}
isSubscribeButton={true} isSubscribeButton={true}
class={clsx(styles.actionButton, { class={clsx(styles.actionButton, {
[styles.isSubscribing]: [styles.isFollowing]:
subscribeInAction()?.slug === props.topic.slug ? subscribeInAction().type : undefined, following()?.slug === props.topic.slug ? following().type : undefined,
[stylesButton.subscribed]: isSubscribed(), [stylesButton.followed]: isFollowed(),
})} })}
/> />
</Show> </Show>

View File

@ -17,14 +17,13 @@ type Props = {
export const FullTopic = (props: Props) => { export const FullTopic = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { subscriptions, setFollowing } = useFollowing() const { follows, changeFollowing } = useFollowing()
const { requireAuthentication } = useSession() const { requireAuthentication } = useSession()
const [followed, setFollowed] = createSignal() const [followed, setFollowed] = createSignal()
createEffect(() => { createEffect(() => {
const subs = subscriptions if (follows?.topics.length !== 0) {
if (subs?.topics.length !== 0) { const items = follows.topics || []
const items = subs.topics || []
setFollowed(items.some((x: Topic) => x?.slug === props.topic?.slug)) setFollowed(items.some((x: Topic) => x?.slug === props.topic?.slug))
} }
}) })
@ -33,7 +32,7 @@ export const FullTopic = (props: Props) => {
const really = !followed() const really = !followed()
setFollowed(really) setFollowed(really)
requireAuthentication(() => { requireAuthentication(() => {
setFollowing(FollowingEntity.Topic, props.topic.slug, really) changeFollowing(FollowingEntity.Topic, props.topic.slug, really)
}, 'follow') }, 'follow')
} }

View File

@ -45,6 +45,7 @@
.info { .info {
@include font-size(1.4rem); @include font-size(1.4rem);
border: none; border: none;
// display: flex; // display: flex;
@ -62,11 +63,13 @@
.title { .title {
@include font-size(2.2rem); @include font-size(2.2rem);
font-weight: bold; font-weight: bold;
} }
.description { .description {
@include font-size(1.6rem); @include font-size(1.6rem);
line-height: 1.4; line-height: 1.4;
margin: 0.8rem 0; margin: 0.8rem 0;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
@ -104,6 +107,7 @@
.title { .title {
@include font-size(1.4rem); @include font-size(1.4rem);
font-weight: 500; font-weight: 500;
line-height: 1em; line-height: 1em;
color: var(--blue-500); color: var(--blue-500);
@ -111,8 +115,9 @@
} }
.description { .description {
color: var(--black-400);
@include font-size(1.2rem); @include font-size(1.2rem);
color: var(--black-400);
font-weight: 500; font-weight: 500;
margin: 0; margin: 0;
} }

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'
@ -8,12 +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 { BadgeSubscribeButton } from '../../_shared/BadgeSubscribeButton' import { FollowingButton } from '../../_shared/FollowingButton'
import styles from './TopicBadge.module.scss' import styles from './TopicBadge.module.scss'
type Props = { type Props = {
topic: Topic topic: Topic
minimizeSubscribeButton?: boolean minimize?: boolean
showStat?: boolean showStat?: boolean
subscriptionsMode?: boolean subscriptionsMode?: boolean
} }
@ -23,18 +23,25 @@ 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 [isSubscribed, setIsSubscribed] = createSignal<boolean>() const [isFollowed, setIsFollowed] = createSignal<boolean>()
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing() const { follow, unfollow, follows, following } = useFollowing()
createEffect(() => { createEffect(
if (!(subscriptions && props.topic)) return on(
const subscribed = subscriptions.topics?.some((topics) => topics.id === props.topic?.id) [() => follows, () => props.topic],
setIsSubscribed(subscribed) ([flws, tpc]) => {
}) if (flws && tpc) {
const followed = follows?.topics?.some((topics) => topics.id === props.topic?.id)
setIsFollowed(followed)
}
},
{ defer: true },
),
)
const handleFollowClick = () => { const handleFollowClick = () => {
requireAuthentication(() => { requireAuthentication(() => {
isSubscribed() isFollowed()
? follow(FollowingEntity.Topic, props.topic.slug) ? follow(FollowingEntity.Topic, props.topic.slug)
: unfollow(FollowingEntity.Topic, props.topic.slug) : unfollow(FollowingEntity.Topic, props.topic.slug)
}, 'subscribe') }, 'subscribe')
@ -82,12 +89,10 @@ export const TopicBadge = (props: Props) => {
</a> </a>
</div> </div>
<div class={styles.actions}> <div class={styles.actions}>
<BadgeSubscribeButton <FollowingButton
isSubscribed={isSubscribed()} isFollowed={isFollowed()}
action={handleFollowClick} action={handleFollowClick}
actionMessageType={ actionMessageType={following()?.slug === props.topic.slug ? following().type : undefined}
subscribeInAction()?.slug === props.topic.slug ? subscribeInAction().type : undefined
}
/> />
</div> </div>
</div> </div>

View File

@ -39,54 +39,55 @@ const LOAD_MORE_PAGE_SIZE = 9
export const AuthorView = (props: Props) => { export const AuthorView = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { followers: myFollowers } = useFollowing() const { followers: myFollowers, follows: myFollows } = useFollowing()
const { session } = useSession() const { session, author: me } = useSession()
const { sortedArticles } = useArticlesStore({ shouts: props.shouts }) const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
const { page: getPage, searchParams } = useRouter() const { page: getPage, searchParams } = useRouter()
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const [isBioExpanded, setIsBioExpanded] = createSignal(false) const [isBioExpanded, setIsBioExpanded] = createSignal(false)
const [author, setAuthor] = createSignal<Author>() const [author, setAuthor] = createSignal<Author>()
const [followers, setFollowers] = createSignal([]) const [followers, setFollowers] = createSignal([])
const [following, setFollowing] = createSignal<Array<Author | Topic>>([]) // flat AuthorFollowsResult const [following, changeFollowing] = createSignal<Array<Author | Topic>>([]) // flat AuthorFollowsResult
const [showExpandBioControl, setShowExpandBioControl] = createSignal(false) const [showExpandBioControl, setShowExpandBioControl] = createSignal(false)
const [commented, setCommented] = createSignal<Reaction[]>() const [commented, setCommented] = createSignal<Reaction[]>()
const modal = MODALS[searchParams().m] const modal = MODALS[searchParams().m]
const [sessionChecked, setSessionChecked] = createSignal(false) const [sessionChecked, setSessionChecked] = createSignal(false)
createEffect(() => { createEffect(
if ( on(
!sessionChecked() && [() => sessionChecked(), () => props.authorSlug, () => session()?.user?.app_data?.profile?.slug],
props.authorSlug && ([checked, slug, mySlug]) => {
session()?.user?.app_data?.profile?.slug === props.authorSlug if (!checked && slug && mySlug === slug) {
) {
setSessionChecked(true) setSessionChecked(true)
const appdata = session()?.user.app_data const appdata = session()?.user.app_data
if (appdata) { if (appdata) {
console.info('preloaded my own profile') console.info('preloaded my own profile')
const { authors, profile, topics } = appdata setFollowers(myFollowers())
setFollowers(myFollowers) setAuthor(me())
setAuthor(profile) const { authors, topics } = myFollows
setFollowing([...authors, ...topics]) changeFollowing([...authors, ...topics])
} }
} }
}) },
{ defer: true },
),
)
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: string) => { const fetchData = async (slug: string) => {
try { try {
const [subscriptionsResult, followersResult, authorResult] = await Promise.all([ const [followsResult, followersResult, authorResult] = await Promise.all([
apiClient.getAuthorFollows({ slug }), apiClient.getAuthorFollows({ slug }),
apiClient.getAuthorFollowers({ slug }), apiClient.getAuthorFollowers({ slug }),
loadAuthor({ slug }), loadAuthor({ slug }),
]) ])
const { authors, topics } = subscriptionsResult
setAuthor(authorResult)
setFollowing([...(authors || []), ...(topics || [])])
setFollowers(followersResult || [])
console.info('[components.Author] data loaded') console.info('[components.Author] data loaded')
setAuthor(authorResult)
setFollowers(followersResult || [])
const { authors, topics } = followsResult
changeFollowing([...(authors || []), ...(topics || [])])
} catch (error) { } catch (error) {
console.error('[components.Author] fetch error', error) console.error('[components.Author] fetch error', error)
} }

View File

@ -1,12 +1,11 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Show, createEffect, createSignal } from 'solid-js' import { For, 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'
import { Author, Topic } from '../../../graphql/schema/core.gen' import { Author, Topic } from '../../../graphql/schema/core.gen'
import { SubscriptionFilter } from '../../../pages/types' import { FollowsFilter } from '../../../pages/types'
import { dummyFilter } from '../../../utils/dummyFilter' import { dummyFilter } from '../../../utils/dummyFilter'
// TODO: refactor styles
import { isAuthor } from '../../../utils/isAuthor' import { isAuthor } from '../../../utils/isAuthor'
import { AuthorBadge } from '../../Author/AuthorBadge' import { AuthorBadge } from '../../Author/AuthorBadge'
import { ProfileSettingsNavigation } from '../../Nav/ProfileSettingsNavigation' import { ProfileSettingsNavigation } from '../../Nav/ProfileSettingsNavigation'
@ -19,30 +18,30 @@ import stylesSettings from '../../../styles/FeedSettings.module.scss'
export const ProfileSubscriptions = () => { export const ProfileSubscriptions = () => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const { subscriptions } = useFollowing() const { follows } = useFollowing()
const [following, setFollowing] = createSignal<Array<Author | Topic>>([]) const [flatFollows, setFlatFollows] = createSignal<Array<Author | Topic>>([])
const [filtered, setFiltered] = createSignal<Array<Author | Topic>>([]) const [filtered, setFiltered] = createSignal<Array<Author | Topic>>([])
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all') const [followsFilter, setFollowsFilter] = createSignal<FollowsFilter>('all')
const [searchQuery, setSearchQuery] = createSignal('') const [searchQuery, setSearchQuery] = createSignal('')
createEffect(() => { createEffect(() => setFlatFollows([...(follows?.authors || []), ...(follows?.topics || [])]))
const { authors, topics } = subscriptions
if (authors || topics) { createEffect(
const fdata = [...(authors || []), ...(topics || [])] on([flatFollows, followsFilter], ([flat, mode]) => {
setFollowing(fdata) if (mode === 'authors') {
if (subscriptionFilter() === 'authors') { setFiltered(flat.filter((s) => 'name' in s))
setFiltered(fdata.filter((s) => 'name' in s)) } else if (mode === 'topics') {
} else if (subscriptionFilter() === 'topics') { setFiltered(flat.filter((s) => 'title' in s))
setFiltered(fdata.filter((s) => 'title' in s))
} else { } else {
setFiltered(fdata) setFiltered(flat)
} }
} }),
}) { defer: true },
)
createEffect(() => { createEffect(() => {
if (searchQuery()) { if (searchQuery()) {
setFiltered(dummyFilter(following(), searchQuery(), lang())) setFiltered(dummyFilter(flatFollows(), searchQuery(), lang()))
} }
}) })
@ -60,32 +59,32 @@ export const ProfileSubscriptions = () => {
<div class="col-md-20 col-lg-18 col-xl-16"> <div class="col-md-20 col-lg-18 col-xl-16">
<h1>{t('My subscriptions')}</h1> <h1>{t('My subscriptions')}</h1>
<p class="description">{t('Here you can manage all your Discours subscriptions')}</p> <p class="description">{t('Here you can manage all your Discours subscriptions')}</p>
<Show when={following()} fallback={<Loading />}> <Show when={flatFollows()} fallback={<Loading />}>
<ul class="view-switcher"> <ul class="view-switcher">
<li <li
class={clsx({ class={clsx({
'view-switcher__item--selected': subscriptionFilter() === 'all', 'view-switcher__item--selected': followsFilter() === 'all',
})} })}
> >
<button type="button" onClick={() => setSubscriptionFilter('all')}> <button type="button" onClick={() => setFollowsFilter('all')}>
{t('All')} {t('All')}
</button> </button>
</li> </li>
<li <li
class={clsx({ class={clsx({
'view-switcher__item--selected': subscriptionFilter() === 'authors', 'view-switcher__item--selected': followsFilter() === 'authors',
})} })}
> >
<button type="button" onClick={() => setSubscriptionFilter('authors')}> <button type="button" onClick={() => setFollowsFilter('authors')}>
{t('Authors')} {t('Authors')}
</button> </button>
</li> </li>
<li <li
class={clsx({ class={clsx({
'view-switcher__item--selected': subscriptionFilter() === 'topics', 'view-switcher__item--selected': followsFilter() === 'topics',
})} })}
> >
<button type="button" onClick={() => setSubscriptionFilter('topics')}> <button type="button" onClick={() => setFollowsFilter('topics')}>
{t('Topics')} {t('Topics')}
</button> </button>
</li> </li>
@ -104,9 +103,9 @@ export const ProfileSubscriptions = () => {
{(followingItem) => ( {(followingItem) => (
<div> <div>
{isAuthor(followingItem) ? ( {isAuthor(followingItem) ? (
<AuthorBadge minimizeSubscribeButton={true} author={followingItem} /> <AuthorBadge minimize={true} author={followingItem} />
) : ( ) : (
<TopicBadge minimizeSubscribeButton={true} topic={followingItem} /> <TopicBadge minimize={true} topic={followingItem} />
)} )}
</div> </div>
)} )}

View File

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

View File

@ -175,7 +175,7 @@
} }
} }
&.subscribed { &.followed {
background: #fff; background: #fff;
color: #000; color: #000;

View File

@ -2,35 +2,36 @@ import { clsx } from 'clsx'
import { Show, createMemo } from 'solid-js' import { Show, createMemo } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Button } from '../Button' import { Button } from '../Button'
import stylesButton from '../Button/Button.module.scss'
import { CheckButton } from '../CheckButton' import { CheckButton } from '../CheckButton'
import { Icon } from '../Icon' import { Icon } from '../Icon'
import styles from './BadgeDubscribeButton.module.scss'
import stylesButton from '../Button/Button.module.scss'
import styles from './FollowingButton.module.scss'
type Props = { type Props = {
class?: string class?: string
isSubscribed: boolean isFollowed: boolean
minimizeSubscribeButton?: boolean minimize?: boolean
action: () => void action: () => void
iconButtons?: boolean iconButtons?: boolean
actionMessageType?: 'subscribe' | 'unsubscribe' actionMessageType?: 'follow' | 'unfollow'
} }
export const BadgeSubscribeButton = (props: Props) => { export const FollowingButton = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const inActionText = createMemo(() => { const inActionText = createMemo(() => {
return props.actionMessageType === 'subscribe' ? t('Subscribing...') : t('Unsubscribing...') return props.actionMessageType === 'follow' ? t('Following...') : t('Unfollowing...')
}) })
return ( return (
<div class={props.class}> <div class={props.class}>
<Show <Show
when={!props.minimizeSubscribeButton} when={!props.minimize}
fallback={<CheckButton text={t('Follow')} checked={props.isSubscribed} onClick={props.action} />} fallback={<CheckButton text={t('Follow')} checked={props.isFollowed} onClick={props.action} />}
> >
<Show <Show
when={props.isSubscribed} when={props.isFollowed}
fallback={ fallback={
<Button <Button
variant={props.iconButtons ? 'secondary' : 'bordered'} variant={props.iconButtons ? 'secondary' : 'bordered'}
@ -38,7 +39,7 @@ export const BadgeSubscribeButton = (props: Props) => {
value={ value={
<Show <Show
when={props.iconButtons} when={props.iconButtons}
fallback={props.actionMessageType ? inActionText() : t('Subscribe')} fallback={props.actionMessageType ? inActionText() : t('Follow')}
> >
<Icon name="author-subscribe" class={stylesButton.icon} /> <Icon name="author-subscribe" class={stylesButton.icon} />
</Show> </Show>
@ -47,7 +48,7 @@ export const BadgeSubscribeButton = (props: Props) => {
isSubscribeButton={true} isSubscribeButton={true}
class={clsx(styles.actionButton, { class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons, [styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: props.isSubscribed, [stylesButton.followed]: props.isFollowed,
})} })}
/> />
} }
@ -76,7 +77,7 @@ export const BadgeSubscribeButton = (props: Props) => {
isSubscribeButton={true} isSubscribeButton={true}
class={clsx(styles.actionButton, { class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons, [styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: props.isSubscribed, [stylesButton.followed]: props.isFollowed,
})} })}
/> />
</Show> </Show>

View File

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

View File

@ -1,30 +1,27 @@
import { Accessor, JSX, createContext, createEffect, createSignal, useContext } from 'solid-js' import { Accessor, JSX, createContext, createEffect, createSignal, on, 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, Community, FollowingEntity, Topic } from '../graphql/schema/core.gen' import { Author, AuthorFollowsResult, FollowingEntity } from '../graphql/schema/core.gen'
import { useSession } from './session' import { useSession } from './session'
export type SubscriptionsData = { type FollowingData = { slug: string; type: 'follow' | 'unfollow' }
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<Author[]> followers: Accessor<Author[]>
subscriptions: AuthorFollowsResult setFollows: (follows: AuthorFollowsResult) => void
setSubscriptions: (subscriptions: AuthorFollowsResult) => void
setFollowing: (what: FollowingEntity, slug: string, value: boolean) => void following: Accessor<FollowingData>
loadSubscriptions: () => void changeFollowing: (what: FollowingEntity, slug: string, value: boolean) => void
follows: AuthorFollowsResult
loadFollows: () => 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>
// followers: Accessor<Author[]>
subscribeInAction?: Accessor<SubscribeAction>
} }
const FollowingContext = createContext<FollowingContextType>() const FollowingContext = createContext<FollowingContextType>()
@ -42,7 +39,7 @@ const EMPTY_SUBSCRIPTIONS: AuthorFollowsResult = {
export const FollowingProvider = (props: { children: JSX.Element }) => { export const FollowingProvider = (props: { children: JSX.Element }) => {
const [loading, setLoading] = createSignal<boolean>(false) const [loading, setLoading] = createSignal<boolean>(false)
const [followers, setFollowers] = createSignal<Author[]>([]) const [followers, setFollowers] = createSignal<Author[]>([])
const [subscriptions, setSubscriptions] = createStore<AuthorFollowsResult>(EMPTY_SUBSCRIPTIONS) const [follows, setFollows] = createStore<AuthorFollowsResult>(EMPTY_SUBSCRIPTIONS)
const { author, session } = useSession() const { author, session } = useSession()
const fetchData = async () => { const fetchData = async () => {
@ -51,7 +48,7 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
if (apiClient.private) { if (apiClient.private) {
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) setFollows(result || EMPTY_SUBSCRIPTIONS)
} }
} catch (error) { } catch (error) {
console.info('[context.following] cannot get subs', error) console.info('[context.following] cannot get subs', error)
@ -60,47 +57,51 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
} }
} }
createEffect(() => { const [following, setFollowing] = createSignal<FollowingData>()
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' }) setFollowing({ slug, type: 'follow' })
try { try {
const subscriptionData = await apiClient.follow({ what, slug }) const result = await apiClient.follow({ what, slug })
setSubscriptions((prevSubscriptions) => { setFollows((subs) => {
if (!prevSubscriptions[what]) prevSubscriptions[what] = [] if (result.authors) subs['authors'] = result.authors || []
prevSubscriptions[what].push(subscriptionData) if (result.topics) subs['topics'] = result.topics || []
return prevSubscriptions return subs
}) })
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} finally { } finally {
setSubscribeInAction() // Сбрасываем состояние действия подписки. setFollowing() // Сбрасываем состояние действия подписки.
} }
} }
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' }) setFollowing({ slug: slug, type: 'unfollow' })
try { try {
await apiClient.unfollow({ what, slug }) const result = await apiClient.unfollow({ what, slug })
setFollows((subs) => {
if (result.authors) subs['authors'] = result.authors || []
if (result.topics) subs['topics'] = result.topics || []
return subs
})
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} finally { } finally {
setSubscribeInAction() setFollowing()
} }
} }
createEffect(() => { createEffect(
if (author()) { on(
() => author(),
(a) => {
if (a?.id) {
try { try {
const appdata = session()?.user.app_data const appdata = session()?.user.app_data
if (appdata) { if (appdata) {
const { authors, followers, topics } = appdata const { authors, followers, topics } = appdata
setSubscriptions({ authors, topics }) setFollows({ authors, topics })
setFollowers(followers) setFollowers(followers)
if (!authors) fetchData() if (!authors) fetchData()
} }
@ -108,11 +109,13 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
console.error(e) console.error(e)
} }
} }
}) },
),
)
const setFollowing = (what: FollowingEntity, slug: string, value = true) => { const changeFollowing = (what: FollowingEntity, slug: string, value = true) => {
setSubscriptions((prevSubscriptions) => { setFollows((fff) => {
const updatedSubs = { ...prevSubscriptions } const updatedSubs = { ...fff }
if (!updatedSubs[what]) updatedSubs[what] = [] if (!updatedSubs[what]) updatedSubs[what] = []
if (value) { if (value) {
const exists = updatedSubs[what]?.some((entity) => entity.slug === slug) const exists = updatedSubs[what]?.some((entity) => entity.slug === slug)
@ -133,15 +136,14 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
const value: FollowingContextType = { const value: FollowingContextType = {
loading, loading,
subscriptions, follows,
setSubscriptions, setFollows,
setFollowing, following,
changeFollowing,
followers, followers,
loadSubscriptions: fetchData, loadFollows: 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

@ -6,7 +6,24 @@ export default gql`
error error
authors { authors {
id id
name
slug slug
pic
bio
stat {
followers
shouts
comments
}
}
topics {
body
slug
stat {
shouts
authors
followers
}
} }
} }
} }

View File

@ -3,6 +3,27 @@ export default gql`
mutation UnfollowMutation($what: FollowingEntity!, $slug: String!) { mutation UnfollowMutation($what: FollowingEntity!, $slug: String!) {
unfollow(what: $what, slug: $slug) { unfollow(what: $what, slug: $slug) {
error error
authors {
id
name
slug
pic
bio
stat {
followers
shouts
comments
}
}
topics {
body
slug
stat {
shouts
authors
followers
}
}
} }
} }
` `

View File

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

View File

@ -14,8 +14,9 @@ const cssModuleHMR = () => {
const { modules } = context const { modules } = context
modules.forEach((module) => { modules.forEach((module) => {
if (module.id.includes('.module.scss')) { if (module.id.includes('.scss') || module.id.includes('.css')) {
module.isSelfAccepting = true module.isSelfAccepting = true
module.accept()
} }
}) })
}, },