frontend-rw #1
8 changed files with 70 additions and 322 deletions
|
@ -1,198 +0,0 @@
|
|||
import { getClient } from '../api';
|
||||
|
||||
import type { PaginatedResponse, Tag } from 'pl-api';
|
||||
import type { AppDispatch, RootState } from 'pl-fe/store';
|
||||
|
||||
const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST' as const;
|
||||
const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS' as const;
|
||||
const HASHTAG_FETCH_FAIL = 'HASHTAG_FETCH_FAIL' as const;
|
||||
|
||||
const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST' as const;
|
||||
const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS' as const;
|
||||
const HASHTAG_FOLLOW_FAIL = 'HASHTAG_FOLLOW_FAIL' as const;
|
||||
|
||||
const HASHTAG_UNFOLLOW_REQUEST = 'HASHTAG_UNFOLLOW_REQUEST' as const;
|
||||
const HASHTAG_UNFOLLOW_SUCCESS = 'HASHTAG_UNFOLLOW_SUCCESS' as const;
|
||||
const HASHTAG_UNFOLLOW_FAIL = 'HASHTAG_UNFOLLOW_FAIL' as const;
|
||||
|
||||
const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST' as const;
|
||||
const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS' as const;
|
||||
const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL' as const;
|
||||
|
||||
const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST' as const;
|
||||
const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS' as const;
|
||||
const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL' as const;
|
||||
|
||||
const fetchHashtag = (name: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchHashtagRequest());
|
||||
|
||||
return getClient(getState()).myAccount.getTag(name).then((data) => {
|
||||
dispatch(fetchHashtagSuccess(name, data));
|
||||
}).catch(err => {
|
||||
dispatch(fetchHashtagFail(err));
|
||||
});
|
||||
};
|
||||
|
||||
const fetchHashtagRequest = () => ({
|
||||
type: HASHTAG_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
const fetchHashtagSuccess = (name: string, tag: Tag) => ({
|
||||
type: HASHTAG_FETCH_SUCCESS,
|
||||
name,
|
||||
tag,
|
||||
});
|
||||
|
||||
const fetchHashtagFail = (error: unknown) => ({
|
||||
type: HASHTAG_FETCH_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
const followHashtag = (name: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(followHashtagRequest(name));
|
||||
|
||||
return getClient(getState()).myAccount.followTag(name).then((data) => {
|
||||
dispatch(followHashtagSuccess(name, data));
|
||||
}).catch(err => {
|
||||
dispatch(followHashtagFail(name, err));
|
||||
});
|
||||
};
|
||||
|
||||
const followHashtagRequest = (name: string) => ({
|
||||
type: HASHTAG_FOLLOW_REQUEST,
|
||||
name,
|
||||
});
|
||||
|
||||
const followHashtagSuccess = (name: string, tag: Tag) => ({
|
||||
type: HASHTAG_FOLLOW_SUCCESS,
|
||||
name,
|
||||
tag,
|
||||
});
|
||||
|
||||
const followHashtagFail = (name: string, error: unknown) => ({
|
||||
type: HASHTAG_FOLLOW_FAIL,
|
||||
name,
|
||||
error,
|
||||
});
|
||||
|
||||
const unfollowHashtag = (name: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(unfollowHashtagRequest(name));
|
||||
|
||||
return getClient(getState()).myAccount.unfollowTag(name).then((data) => {
|
||||
dispatch(unfollowHashtagSuccess(name, data));
|
||||
}).catch(err => {
|
||||
dispatch(unfollowHashtagFail(name, err));
|
||||
});
|
||||
};
|
||||
|
||||
const unfollowHashtagRequest = (name: string) => ({
|
||||
type: HASHTAG_UNFOLLOW_REQUEST,
|
||||
name,
|
||||
});
|
||||
|
||||
const unfollowHashtagSuccess = (name: string, tag: Tag) => ({
|
||||
type: HASHTAG_UNFOLLOW_SUCCESS,
|
||||
name,
|
||||
tag,
|
||||
});
|
||||
|
||||
const unfollowHashtagFail = (name: string, error: unknown) => ({
|
||||
type: HASHTAG_UNFOLLOW_FAIL,
|
||||
name,
|
||||
error,
|
||||
});
|
||||
|
||||
const fetchFollowedHashtags = () => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchFollowedHashtagsRequest());
|
||||
|
||||
return getClient(getState()).myAccount.getFollowedTags().then(response => {
|
||||
dispatch(fetchFollowedHashtagsSuccess(response.items, response.next));
|
||||
}).catch(err => {
|
||||
dispatch(fetchFollowedHashtagsFail(err));
|
||||
});
|
||||
};
|
||||
|
||||
const fetchFollowedHashtagsRequest = () => ({
|
||||
type: FOLLOWED_HASHTAGS_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
const fetchFollowedHashtagsSuccess = (followed_tags: Array<Tag>, next: (() => Promise<PaginatedResponse<Tag>>) | null) => ({
|
||||
type: FOLLOWED_HASHTAGS_FETCH_SUCCESS,
|
||||
followed_tags,
|
||||
next,
|
||||
});
|
||||
|
||||
const fetchFollowedHashtagsFail = (error: unknown) => ({
|
||||
type: FOLLOWED_HASHTAGS_FETCH_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
const expandFollowedHashtags = () => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const next = getState().followed_tags.next;
|
||||
|
||||
if (next === null) return;
|
||||
|
||||
dispatch(expandFollowedHashtagsRequest());
|
||||
|
||||
return next().then(response => {
|
||||
dispatch(expandFollowedHashtagsSuccess(response.items, response.next));
|
||||
}).catch(error => {
|
||||
dispatch(expandFollowedHashtagsFail(error));
|
||||
});
|
||||
};
|
||||
|
||||
const expandFollowedHashtagsRequest = () => ({
|
||||
type: FOLLOWED_HASHTAGS_EXPAND_REQUEST,
|
||||
});
|
||||
|
||||
const expandFollowedHashtagsSuccess = (followed_tags: Array<Tag>, next: (() => Promise<PaginatedResponse<Tag>>) | null) => ({
|
||||
type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
|
||||
followed_tags,
|
||||
next,
|
||||
});
|
||||
|
||||
const expandFollowedHashtagsFail = (error: unknown) => ({
|
||||
type: FOLLOWED_HASHTAGS_EXPAND_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
type TagsAction =
|
||||
| ReturnType<typeof fetchHashtagRequest>
|
||||
| ReturnType<typeof fetchHashtagSuccess>
|
||||
| ReturnType<typeof fetchHashtagFail>
|
||||
| ReturnType<typeof followHashtagRequest>
|
||||
| ReturnType<typeof followHashtagSuccess>
|
||||
| ReturnType<typeof followHashtagFail>
|
||||
| ReturnType<typeof unfollowHashtagRequest>
|
||||
| ReturnType<typeof unfollowHashtagSuccess>
|
||||
| ReturnType<typeof unfollowHashtagFail>
|
||||
| ReturnType<typeof fetchFollowedHashtagsRequest>
|
||||
| ReturnType<typeof fetchFollowedHashtagsSuccess>
|
||||
| ReturnType<typeof fetchFollowedHashtagsFail>
|
||||
| ReturnType<typeof expandFollowedHashtagsRequest>
|
||||
| ReturnType<typeof expandFollowedHashtagsSuccess>
|
||||
| ReturnType<typeof expandFollowedHashtagsFail>;
|
||||
|
||||
export {
|
||||
HASHTAG_FETCH_REQUEST,
|
||||
HASHTAG_FETCH_SUCCESS,
|
||||
HASHTAG_FETCH_FAIL,
|
||||
HASHTAG_FOLLOW_REQUEST,
|
||||
HASHTAG_FOLLOW_SUCCESS,
|
||||
HASHTAG_FOLLOW_FAIL,
|
||||
HASHTAG_UNFOLLOW_REQUEST,
|
||||
HASHTAG_UNFOLLOW_SUCCESS,
|
||||
HASHTAG_UNFOLLOW_FAIL,
|
||||
FOLLOWED_HASHTAGS_FETCH_REQUEST,
|
||||
FOLLOWED_HASHTAGS_FETCH_SUCCESS,
|
||||
FOLLOWED_HASHTAGS_FETCH_FAIL,
|
||||
FOLLOWED_HASHTAGS_EXPAND_REQUEST,
|
||||
FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
|
||||
FOLLOWED_HASHTAGS_EXPAND_FAIL,
|
||||
fetchHashtag,
|
||||
followHashtag,
|
||||
unfollowHashtag,
|
||||
fetchFollowedHashtags,
|
||||
expandFollowedHashtags,
|
||||
type TagsAction,
|
||||
};
|
|
@ -1,34 +1,20 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { fetchFollowedHashtags, expandFollowedHashtags } from 'pl-fe/actions/tags';
|
||||
import Hashtag from 'pl-fe/components/hashtag';
|
||||
import ScrollableList from 'pl-fe/components/scrollable-list';
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
import PlaceholderHashtag from 'pl-fe/features/placeholder/components/placeholder-hashtag';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { useFollowedTags } from 'pl-fe/queries/hashtags/use-followed-tags';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.followed_tags', defaultMessage: 'Followed hashtags' },
|
||||
});
|
||||
|
||||
const handleLoadMore = debounce((dispatch) => {
|
||||
dispatch(expandFollowedHashtags());
|
||||
}, 300, { leading: true });
|
||||
|
||||
const FollowedTags = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchFollowedHashtags());
|
||||
}, []);
|
||||
|
||||
const tags = useAppSelector((state => state.followed_tags.items));
|
||||
const isLoading = useAppSelector((state => state.followed_tags.isLoading));
|
||||
const hasMore = useAppSelector((state => !!state.followed_tags.next));
|
||||
const { data: tags = [], isLoading, hasNextPage, fetchNextPage } = useFollowedTags();
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.followed_tags' defaultMessage="You haven't followed any hashtag yet." />;
|
||||
|
||||
|
@ -37,8 +23,8 @@ const FollowedTags = () => {
|
|||
<ScrollableList
|
||||
emptyMessage={emptyMessage}
|
||||
isLoading={isLoading}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={() => handleLoadMore(dispatch)}
|
||||
hasMore={hasNextPage}
|
||||
onLoadMore={fetchNextPage}
|
||||
placeholderComponent={PlaceholderHashtag}
|
||||
placeholderCount={5}
|
||||
itemClassName='pb-3'
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { fetchHashtag, followHashtag, unfollowHashtag } from 'pl-fe/actions/tags';
|
||||
import { fetchHashtagTimeline, clearTimeline } from 'pl-fe/actions/timelines';
|
||||
import { useHashtagStream } from 'pl-fe/api/hooks/streaming/use-hashtag-stream';
|
||||
import List, { ListItem } from 'pl-fe/components/list';
|
||||
|
@ -9,11 +8,12 @@ import Column from 'pl-fe/components/ui/column';
|
|||
import Toggle from 'pl-fe/components/ui/toggle';
|
||||
import Timeline from 'pl-fe/features/ui/components/timeline';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useIsMobile } from 'pl-fe/hooks/use-is-mobile';
|
||||
import { useLoggedIn } from 'pl-fe/hooks/use-logged-in';
|
||||
import { useTheme } from 'pl-fe/hooks/use-theme';
|
||||
import { useFollowHashtagMutation, useUnfollowHashtagMutation } from 'pl-fe/queries/hashtags/use-followed-tags';
|
||||
import { useHashtag } from 'pl-fe/queries/hashtags/use-hashtag';
|
||||
|
||||
interface IHashtagTimeline {
|
||||
params?: {
|
||||
|
@ -26,20 +26,23 @@ const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
|||
|
||||
const features = useFeatures();
|
||||
const dispatch = useAppDispatch();
|
||||
const tag = useAppSelector((state) => state.tags[tagId]);
|
||||
const { data: tag } = useHashtag(tagId);
|
||||
const { isLoggedIn } = useLoggedIn();
|
||||
const theme = useTheme();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const { mutate: followHashtag } = useFollowHashtagMutation(tagId);
|
||||
const { mutate: unfollowHashtag } = useUnfollowHashtagMutation(tagId);
|
||||
|
||||
const handleLoadMore = () => {
|
||||
dispatch(fetchHashtagTimeline(tagId, { }, true));
|
||||
};
|
||||
|
||||
const handleFollow = () => {
|
||||
if (tag?.following) {
|
||||
dispatch(unfollowHashtag(tagId));
|
||||
unfollowHashtag();
|
||||
} else {
|
||||
dispatch(followHashtag(tagId));
|
||||
followHashtag();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -47,7 +50,6 @@ const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
|||
|
||||
useEffect(() => {
|
||||
dispatch(clearTimeline(`hashtag:${tagId}`));
|
||||
dispatch(fetchHashtag(tagId));
|
||||
dispatch(fetchHashtagTimeline(tagId));
|
||||
}, [tagId]);
|
||||
|
||||
|
|
43
packages/pl-fe/src/queries/hashtags/use-followed-tags.ts
Normal file
43
packages/pl-fe/src/queries/hashtags/use-followed-tags.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useClient } from 'pl-fe/hooks/use-client';
|
||||
|
||||
import { queryClient } from '../client';
|
||||
import { makePaginatedResponseQuery } from '../utils/make-paginated-response-query';
|
||||
|
||||
const useFollowedTags = makePaginatedResponseQuery(
|
||||
() => ['followedTags'],
|
||||
(client) => client.myAccount.getFollowedTags(),
|
||||
);
|
||||
|
||||
const useFollowHashtagMutation = (tag: string) => {
|
||||
const client = useClient();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['followedTags', tag.toLocaleLowerCase()],
|
||||
mutationFn: () => client.myAccount.followTag(tag),
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['followedTags'],
|
||||
});
|
||||
queryClient.setQueryData(['hashtags', tag.toLocaleLowerCase()], data);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const useUnfollowHashtagMutation = (tag: string) => {
|
||||
const client = useClient();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['followedTags', tag.toLocaleLowerCase()],
|
||||
mutationFn: () => client.myAccount.unfollowTag(tag),
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['followedTags'],
|
||||
});
|
||||
queryClient.setQueryData(['hashtags', tag.toLocaleLowerCase()], data);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export { useFollowedTags, useFollowHashtagMutation, useUnfollowHashtagMutation };
|
14
packages/pl-fe/src/queries/hashtags/use-hashtag.ts
Normal file
14
packages/pl-fe/src/queries/hashtags/use-hashtag.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useClient } from 'pl-fe/hooks/use-client';
|
||||
|
||||
const useHashtag = (tag: string) => {
|
||||
const client = useClient();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['hashtags', tag.toLocaleLowerCase()],
|
||||
queryFn: () => client.myAccount.getTag(tag),
|
||||
});
|
||||
};
|
||||
|
||||
export { useHashtag };
|
|
@ -1,56 +0,0 @@
|
|||
import { create } from 'mutative';
|
||||
|
||||
import {
|
||||
FOLLOWED_HASHTAGS_FETCH_REQUEST,
|
||||
FOLLOWED_HASHTAGS_FETCH_SUCCESS,
|
||||
FOLLOWED_HASHTAGS_FETCH_FAIL,
|
||||
FOLLOWED_HASHTAGS_EXPAND_REQUEST,
|
||||
FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
|
||||
FOLLOWED_HASHTAGS_EXPAND_FAIL,
|
||||
TagsAction,
|
||||
} from 'pl-fe/actions/tags';
|
||||
|
||||
import type { PaginatedResponse, Tag } from 'pl-api';
|
||||
|
||||
interface State {
|
||||
items: Array<Tag>;
|
||||
isLoading: boolean;
|
||||
next: (() => Promise<PaginatedResponse<Tag>>) | null;
|
||||
}
|
||||
|
||||
const initalState: State = {
|
||||
items: [],
|
||||
isLoading: false,
|
||||
next: null,
|
||||
};
|
||||
|
||||
const followed_tags = (state = initalState, action: TagsAction): State => {
|
||||
switch (action.type) {
|
||||
case FOLLOWED_HASHTAGS_FETCH_REQUEST:
|
||||
case FOLLOWED_HASHTAGS_EXPAND_REQUEST:
|
||||
return create(state, draft => {
|
||||
draft.isLoading = true;
|
||||
});
|
||||
case FOLLOWED_HASHTAGS_FETCH_SUCCESS:
|
||||
return create(state, draft => {
|
||||
draft.items = action.followed_tags;
|
||||
draft.isLoading = true;
|
||||
draft.next = action.next;
|
||||
});
|
||||
case FOLLOWED_HASHTAGS_FETCH_FAIL:
|
||||
case FOLLOWED_HASHTAGS_EXPAND_FAIL:
|
||||
return create(state, draft => {
|
||||
draft.isLoading = false;
|
||||
});
|
||||
case FOLLOWED_HASHTAGS_EXPAND_SUCCESS:
|
||||
return create(state, draft => {
|
||||
draft.items = [...draft.items, ...action.followed_tags];
|
||||
draft.isLoading = true;
|
||||
draft.next = action.next;
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export { followed_tags as default };
|
|
@ -15,7 +15,6 @@ import conversations from './conversations';
|
|||
import domain_lists from './domain-lists';
|
||||
import draft_statuses from './draft-statuses';
|
||||
import filters from './filters';
|
||||
import followed_tags from './followed-tags';
|
||||
import instance from './instance';
|
||||
import listAdder from './list-adder';
|
||||
import listEditor from './list-editor';
|
||||
|
@ -32,7 +31,6 @@ import scheduled_statuses from './scheduled-statuses';
|
|||
import security from './security';
|
||||
import status_lists from './status-lists';
|
||||
import statuses from './statuses';
|
||||
import tags from './tags';
|
||||
import timelines from './timelines';
|
||||
|
||||
const reducers = {
|
||||
|
@ -48,7 +46,6 @@ const reducers = {
|
|||
draft_statuses,
|
||||
entities,
|
||||
filters,
|
||||
followed_tags,
|
||||
instance,
|
||||
listAdder,
|
||||
listEditor,
|
||||
|
@ -65,7 +62,6 @@ const reducers = {
|
|||
security,
|
||||
status_lists,
|
||||
statuses,
|
||||
tags,
|
||||
timelines,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
import { create } from 'mutative';
|
||||
|
||||
import {
|
||||
HASHTAG_FETCH_SUCCESS,
|
||||
HASHTAG_FOLLOW_REQUEST,
|
||||
HASHTAG_FOLLOW_FAIL,
|
||||
HASHTAG_UNFOLLOW_REQUEST,
|
||||
HASHTAG_UNFOLLOW_FAIL,
|
||||
type TagsAction,
|
||||
} from 'pl-fe/actions/tags';
|
||||
|
||||
import type { Tag } from 'pl-api';
|
||||
|
||||
type State = Record<string, Tag>;
|
||||
|
||||
const initialState: State = {};
|
||||
|
||||
const tags = (state = initialState, action: TagsAction) => {
|
||||
switch (action.type) {
|
||||
case HASHTAG_FETCH_SUCCESS:
|
||||
return create(state, (draft) => {
|
||||
draft[action.name] = action.tag;
|
||||
});
|
||||
case HASHTAG_FOLLOW_REQUEST:
|
||||
case HASHTAG_UNFOLLOW_FAIL:
|
||||
return create(state, (draft) => {
|
||||
if (draft[action.name]) draft[action.name].following = true;
|
||||
});
|
||||
case HASHTAG_FOLLOW_FAIL:
|
||||
case HASHTAG_UNFOLLOW_REQUEST:
|
||||
return create(state, (draft) => {
|
||||
if (draft[action.name]) draft[action.name].following = false;
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export { tags as default };
|
Loading…
Reference in a new issue