diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 32f06587..2690613b 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -528,5 +528,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" + "Restore password": "Restore password", + "Subscribing...": "Subscribing...", + "Unsubscribing...": "Unsubscribing..." } diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index fc28e27e..13ba2de4 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -555,5 +555,7 @@ "yesterday": "вчера", "Failed to delete comment": "Не удалось удалить комментарий", "It's OK. Just enter your email to receive a link to change your password": "Ничего страшного. Просто укажите свою почту, чтобы получить ссылку для смены пароля", - "Restore password": "Восстановить пароль" + "Restore password": "Восстановить пароль", + "Subscribing...": "Подписываем...", + "Unsubscribing...": "Отписываем..." } diff --git a/src/components/App.tsx b/src/components/App.tsx index 15ce90bf..726dd596 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -40,6 +40,7 @@ import { InboxPage } from '../pages/inbox.page' import { HomePage } from '../pages/index.page' import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page' import { ProfileSettingsPage } from '../pages/profile/profileSettings.page' +//TODO: ProfileSubscriptionsPage - garbage code? import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page' import { SearchPage } from '../pages/search.page' import { TopicPage } from '../pages/topic.page' diff --git a/src/components/Author/AuthorBadge/AuthorBadge.module.scss b/src/components/Author/AuthorBadge/AuthorBadge.module.scss index 8dc68f4b..ebd5d145 100644 --- a/src/components/Author/AuthorBadge/AuthorBadge.module.scss +++ b/src/components/Author/AuthorBadge/AuthorBadge.module.scss @@ -115,8 +115,4 @@ } } } - - .actionButtonLabelHovered { - display: none; - } } diff --git a/src/components/Author/AuthorBadge/AuthorBadge.tsx b/src/components/Author/AuthorBadge/AuthorBadge.tsx index e0ef0334..bc7c3bbd 100644 --- a/src/components/Author/AuthorBadge/AuthorBadge.tsx +++ b/src/components/Author/AuthorBadge/AuthorBadge.tsx @@ -10,14 +10,12 @@ import { Author, FollowingEntity } from '../../../graphql/schema/core.gen' import { router, useRouter } from '../../../stores/router' import { translit } from '../../../utils/ru2en' import { isCyrillic } from '../../../utils/translate' +import { BadgeSubscribeButton } from '../../_shared/BadgeSubscribeButton' import { Button } from '../../_shared/Button' import { CheckButton } from '../../_shared/CheckButton' import { ConditionalWrapper } from '../../_shared/ConditionalWrapper' import { Icon } from '../../_shared/Icon' import { Userpic } from '../Userpic' - -import { FollowedInfo } from '../../../pages/types' -import stylesButton from '../../_shared/Button/Button.module.scss' import styles from './AuthorBadge.module.scss' type Props = { @@ -29,13 +27,19 @@ type Props = { inviteView?: boolean onInvite?: (id: number) => void selected?: boolean - isFollowed?: FollowedInfo } export const AuthorBadge = (props: Props) => { const { mediaMatches } = useMediaQuery() const { author, requireAuthentication } = useSession() + const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing() const [isMobileView, setIsMobileView] = createSignal(false) - const [isFollowed, setIsFollowed] = createSignal() + const [isSubscribed, setIsSubscribed] = createSignal() + + createEffect(() => { + if (!subscriptions || !props.author) return + const subscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === props.author?.id) + setIsSubscribed(subscribed) + }) createEffect(() => { setIsMobileView(!mediaMatches.sm) @@ -67,20 +71,11 @@ export const AuthorBadge = (props: Props) => { return props.author.name }) - createEffect( - on( - () => props.isFollowed, - () => { - setIsFollowed(props.isFollowed?.value) - }, - ), - ) - const handleFollowClick = () => { - const value = !isFollowed() requireAuthentication(() => { - setIsFollowed(value) - setFollowing(FollowingEntity.Author, props.author.slug, value) + isSubscribed() + ? unfollow(FollowingEntity.Author, props.author.slug) + : follow(FollowingEntity.Author, props.author.slug) }, 'subscribe') } @@ -134,55 +129,13 @@ export const AuthorBadge = (props: Props) => {
- } - > - - - - } - onClick={handleFollowClick} - isSubscribeButton={true} - class={clsx(styles.actionButton, { - [styles.iconed]: props.iconButtons, - [stylesButton.subscribed]: isFollowed(), - })} - /> - } - > -
-
- + - - } - > -
diff --git a/src/components/Views/AllTopics/AllTopics.tsx b/src/components/Views/AllTopics/AllTopics.tsx index fdb8b532..20e14709 100644 --- a/src/components/Views/AllTopics/AllTopics.tsx +++ b/src/components/Views/AllTopics/AllTopics.tsx @@ -74,8 +74,6 @@ export const AllTopics = (props: Props) => { return keys }) - const { isOwnerSubscribed } = useFollowing() - const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE) const [searchQuery, setSearchQuery] = createSignal('') const filteredResults = createMemo(() => { @@ -188,14 +186,7 @@ export const AllTopics = (props: Props) => { {(topic) => ( <> - 0, - value: isOwnerSubscribed(topic.slug), - }} - showStat={true} - /> + )} diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index 4ddb5649..ca2cdd0c 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -3,7 +3,7 @@ import type { Author, Reaction, Shout, Topic } from '../../../graphql/schema/cor import { getPagePath } from '@nanostores/router' import { Meta, Title } from '@solidjs/meta' 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 { useLocalize } from '../../../context/localize' @@ -75,7 +75,7 @@ export const AuthorView = (props: Props) => { const bioContainerRef: { current: HTMLDivElement } = { current: null } const bioWrapperRef: { current: HTMLDivElement } = { current: null } - const fetchData = async (slug) => { + const fetchData = async (slug: string) => { try { const [subscriptionsResult, followersResult, authorResult] = await Promise.all([ apiClient.getAuthorFollows({ slug }), @@ -118,7 +118,6 @@ export const AuthorView = (props: Props) => { // pagination if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) { loadMore() - loadSubscriptions() } }) @@ -135,6 +134,7 @@ export const AuthorView = (props: Props) => { createEffect(() => { if (author()) { + fetchData(author().slug) fetchComments(author()) } }) diff --git a/src/components/_shared/BadgeSubscribeButton/BadgeDubscribeButton.module.scss b/src/components/_shared/BadgeSubscribeButton/BadgeDubscribeButton.module.scss new file mode 100644 index 00000000..b5a7480f --- /dev/null +++ b/src/components/_shared/BadgeSubscribeButton/BadgeDubscribeButton.module.scss @@ -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; +} diff --git a/src/components/_shared/BadgeSubscribeButton/BadgeSubscribeButton.tsx b/src/components/_shared/BadgeSubscribeButton/BadgeSubscribeButton.tsx new file mode 100644 index 00000000..3b1ffded --- /dev/null +++ b/src/components/_shared/BadgeSubscribeButton/BadgeSubscribeButton.tsx @@ -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 ( +
+ } + > + + + + } + onClick={props.action} + isSubscribeButton={true} + class={clsx(styles.actionButton, { + [styles.iconed]: props.iconButtons, + [stylesButton.subscribed]: props.isSubscribed, + })} + /> + } + > +
+ ) +} diff --git a/src/components/_shared/BadgeSubscribeButton/index.ts b/src/components/_shared/BadgeSubscribeButton/index.ts new file mode 100644 index 00000000..b359ecff --- /dev/null +++ b/src/components/_shared/BadgeSubscribeButton/index.ts @@ -0,0 +1 @@ +export { BadgeSubscribeButton } from './BadgeSubscribeButton' diff --git a/src/context/following.tsx b/src/context/following.tsx index fc92565e..7859102b 100644 --- a/src/context/following.tsx +++ b/src/context/following.tsx @@ -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 { 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' +export type SubscriptionsData = { + topics?: Topic[] + authors?: Author[] + communities?: Community[] +} + +type SubscribeAction = { slug: string; type: 'subscribe' | 'unsubscribe' } + interface FollowingContextType { loading: Accessor followers: Accessor> @@ -15,7 +23,8 @@ interface FollowingContextType { loadSubscriptions: () => void follow: (what: FollowingEntity, slug: string) => Promise unfollow: (what: FollowingEntity, slug: string) => Promise - isOwnerSubscribed: (id: number | string) => boolean + // followers: Accessor + subscribeInAction?: Accessor } const FollowingContext = createContext() @@ -43,7 +52,6 @@ export const FollowingProvider = (props: { children: JSX.Element }) => { console.debug('[context.following] fetching subs data...') const result = await apiClient.getAuthorFollows({ user: session()?.user.id }) setSubscriptions(result || EMPTY_SUBSCRIPTIONS) - console.info('[context.following] subs:', subscriptions) } } catch (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() const follow = async (what: FollowingEntity, slug: string) => { if (!author()) return + setSubscribeInAction({ slug, type: 'subscribe' }) try { - await apiClient.follow({ what, slug }) + const subscriptionData = await apiClient.follow({ what, slug }) setSubscriptions((prevSubscriptions) => { - const updatedSubs = { ...prevSubscriptions } - if (!updatedSubs[what]) updatedSubs[what] = [] - const exists = updatedSubs[what]?.some((entity) => entity.slug === slug) - if (!exists) updatedSubs[what].push(slug) - return updatedSubs + if (!prevSubscriptions[what]) prevSubscriptions[what] = [] + prevSubscriptions[what].push(subscriptionData) + return prevSubscriptions }) } catch (error) { console.error(error) + } finally { + setSubscribeInAction() // Сбрасываем состояние действия подписки. } } const unfollow = async (what: FollowingEntity, slug: string) => { if (!author()) return + setSubscribeInAction({ slug: slug, type: 'unsubscribe' }) try { await apiClient.unfollow({ what, slug }) } catch (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 = { loading, subscriptions, setSubscriptions, - isOwnerSubscribed, setFollowing, followers, loadSubscriptions: fetchData, follow, unfollow, + // followers, + subscribeInAction, } return {props.children} diff --git a/src/graphql/mutation/core/follow.ts b/src/graphql/mutation/core/follow.ts index 07ba6472..528dfd46 100644 --- a/src/graphql/mutation/core/follow.ts +++ b/src/graphql/mutation/core/follow.ts @@ -4,6 +4,10 @@ export default gql` mutation FollowMutation($what: FollowingEntity!, $slug: String!) { follow(what: $what, slug: $slug) { error + authors { + id + slug + } } } ` diff --git a/src/pages/types.ts b/src/pages/types.ts index af588d9f..dc4d7132 100644 --- a/src/pages/types.ts +++ b/src/pages/types.ts @@ -53,9 +53,4 @@ export type UploadedFile = { originalFilename?: string } -export type FollowedInfo = { - value?: boolean - loaded?: boolean -} - export type SubscriptionFilter = 'all' | 'authors' | 'topics' | 'communities'