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 525cbd87..51cdead0 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 adcccc5e..ac42b78a 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/Views/Expo/Expo.tsx b/src/components/Views/Expo/Expo.tsx index 82e43dfe..8506bf24 100644 --- a/src/components/Views/Expo/Expo.tsx +++ b/src/components/Views/Expo/Expo.tsx @@ -1,16 +1,14 @@ import { getPagePath } from '@nanostores/router' import { clsx } from 'clsx' -import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js' +import { For, Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js' import { useLocalize } from '../../../context/localize' import { apiClient } from '../../../graphql/client/core' import { LoadShoutsFilters, LoadShoutsOptions, Shout } from '../../../graphql/schema/core.gen' import { LayoutType } from '../../../pages/types' import { router } from '../../../stores/router' -import { loadShouts, resetSortedArticles, useArticlesStore } from '../../../stores/zine/articles' import { getUnixtime } from '../../../utils/getServerDate' import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll' -import { splitToPages } from '../../../utils/splitToPages' import { ArticleCard } from '../../Feed/ArticleCard' import { Button } from '../../_shared/Button' import { ConditionalWrapper } from '../../_shared/ConditionalWrapper' @@ -28,19 +26,12 @@ export const PRERENDERED_ARTICLES_COUNT = 36 const LOAD_MORE_PAGE_SIZE = 12 export const Expo = (props: Props) => { - const [isLoaded, setIsLoaded] = createSignal(Boolean(props.shouts)) + const { t } = useLocalize() const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) - const [favoriteTopArticles, setFavoriteTopArticles] = createSignal([]) const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal([]) - - const { t } = useLocalize() - - const { sortedArticles } = useArticlesStore({ - shouts: isLoaded() ? props.shouts : [], - layout: props.layout, - }) - + const [articlesEndPage, setArticlesEndPage] = createSignal(PRERENDERED_ARTICLES_COUNT) + const [expoShouts, setExpoShouts] = createSignal([]) const getLoadShoutsFilters = (additionalFilters: LoadShoutsFilters = {}): LoadShoutsFilters => { const filters = { ...additionalFilters } @@ -58,15 +49,18 @@ export const Expo = (props: Props) => { const options: LoadShoutsOptions = { filters: getLoadShoutsFilters(), limit: count, - offset: sortedArticles().length, + offset: expoShouts().length, } options.filters = props.layout ? { layouts: [props.layout] } : { layouts: ['audio', 'video', 'image', 'literature'] } - const { hasMore } = await loadShouts(options) + const newShouts = await apiClient.getShouts(options) + const hasMore = newShouts?.length !== options.limit + 1 && newShouts?.length !== 0 setIsLoadMoreButtonVisible(hasMore) + + setExpoShouts((prev) => [...prev, ...newShouts]) } const loadMoreWithoutScrolling = async (count: number) => { @@ -100,19 +94,7 @@ export const Expo = (props: Props) => { } onMount(() => { - if (isLoaded()) { - return - } - loadMore(PRERENDERED_ARTICLES_COUNT + LOAD_MORE_PAGE_SIZE) - setIsLoaded(true) - }) - - onMount(() => { - if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) { - loadMore(LOAD_MORE_PAGE_SIZE) - } - loadRandomTopArticles() loadRandomTopMonthArticles() }) @@ -121,9 +103,11 @@ export const Expo = (props: Props) => { on( () => props.layout, () => { - resetSortedArticles() + setExpoShouts([]) + setIsLoadMoreButtonVisible(false) setFavoriteTopArticles([]) setReactedTopMonthArticles([]) + setArticlesEndPage(PRERENDERED_ARTICLES_COUNT) loadMore(PRERENDERED_ARTICLES_COUNT + LOAD_MORE_PAGE_SIZE) loadRandomTopArticles() loadRandomTopMonthArticles() @@ -132,16 +116,17 @@ export const Expo = (props: Props) => { ) onCleanup(() => { - resetSortedArticles() + setExpoShouts([]) }) const handleLoadMoreClick = () => { loadMoreWithoutScrolling(LOAD_MORE_PAGE_SIZE) + setArticlesEndPage((prev) => prev + LOAD_MORE_PAGE_SIZE) } return (
- 0} fallback={}> + 0} fallback={}>
  • @@ -194,7 +179,7 @@ export const Expo = (props: Props) => {
- + {(shout) => (
{ 0} keyed={true}> - + {(shout) => (
{ 0} keyed={true}> - + {(shout) => (
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'