frontend-rw #1

Merged
marcin merged 347 commits from frontend-rw into develop 2024-12-05 15:32:18 -08:00
8 changed files with 70 additions and 322 deletions
Showing only changes of commit 31b16b01d6 - Show all commits

View file

@ -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,
};

View file

@ -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'

View file

@ -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]);

View 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 };

View 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 };

View file

@ -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 };

View file

@ -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,
};

View file

@ -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 };