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

@ -541,4 +541,4 @@
"You've reached a non-existed page": "You've reached a non-existed page", "You've reached a non-existed page": "You've reached a non-existed page",
"Your email": "Your email", "Your email": "Your email",
"Your name will appear on your profile page and as your signature in publications, comments and responses.": "Your name will appear on your profile page and as your signature in publications, comments and responses" "Your name will appear on your profile page and as your signature in publications, comments and responses.": "Your name will appear on your profile page and as your signature in publications, comments and responses"
} }

View File

@ -568,4 +568,4 @@
"You've successfully logged out": "Вы успешно вышли из аккаунта", "You've successfully logged out": "Вы успешно вышли из аккаунта",
"Your email": "Ваш email", "Your email": "Ваш email",
"Your name will appear on your profile page and as your signature in publications, comments and responses.": "Ваше имя появится на странице вашего профиля и как ваша подпись в публикациях, комментариях и откликах" "Your name will appear on your profile page and as your signature in publications, comments and responses.": "Ваше имя появится на странице вашего профиля и как ваша подпись в публикациях, комментариях и откликах"
} }

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() {
@ -48,20 +48,20 @@ export default Node.create({
return { return {
toggleArticle: toggleArticle:
() => () =>
// eslint-disable-next-line unicorn/consistent-function-scoping // eslint-disable-next-line unicorn/consistent-function-scoping
({ commands }) => { ({ commands }) => {
return commands.toggleWrap('article') return commands.toggleWrap('article')
}, },
setArticleFloat: setArticleFloat:
(value) => (value) =>
({ commands }) => { ({ commands }) => {
return commands.updateAttributes(this.name, { 'data-float': value }) return commands.updateAttributes(this.name, { 'data-float': value })
}, },
setArticleBg: setArticleBg:
(value) => (value) =>
({ commands }) => { ({ commands }) => {
return commands.updateAttributes(this.name, { 'data-bg': value }) return commands.updateAttributes(this.name, { 'data-bg': value })
}, },
} }
}, },
}) })

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:
@ -158,4 +158,4 @@
.cardMode { .cardMode {
margin-bottom: 0; margin-bottom: 0;
} }

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;
} }
@ -160,4 +165,4 @@
word-break: keep-all; 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'
@ -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') setFollowers(myFollowers())
const { authors, profile, topics } = appdata setAuthor(me())
setFollowers(myFollowers) const { authors, topics } = myFollows
setAuthor(profile) changeFollowing([...authors, ...topics])
setFollowing([...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;
@ -192,4 +192,4 @@
} }
} }
} }
} }

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,59 +57,65 @@ 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(
try { () => author(),
const appdata = session()?.user.app_data (a) => {
if (appdata) { if (a?.id) {
const { authors, followers, topics } = appdata try {
setSubscriptions({ authors, topics }) const appdata = session()?.user.app_data
setFollowers(followers) if (appdata) {
if (!authors) fetchData() const { authors, followers, topics } = appdata
setFollows({ authors, topics })
setFollowers(followers)
if (!authors) fetchData()
}
} catch (e) {
console.error(e)
}
} }
} catch (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()
} }
}) })
}, },