Merge remote-tracking branch 'origin/develop' into zod-accounts
This commit is contained in:
commit
011f2eb298
|
@ -12,6 +12,7 @@ import { EXCLUDE_TYPES, NOTIFICATION_TYPES } from 'soapbox/utils/notification';
|
|||
import { joinPublicPath } from 'soapbox/utils/static';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { fetchGroupRelationships } from './groups';
|
||||
import {
|
||||
importFetchedAccount,
|
||||
importFetchedAccounts,
|
||||
|
@ -23,7 +24,7 @@ import { getSettings, saveSettings } from './settings';
|
|||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
import type { APIEntity, Status } from 'soapbox/types/entities';
|
||||
|
||||
const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||
const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
|
||||
|
@ -237,6 +238,9 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
|
|||
dispatch(importFetchedAccounts(Object.values(entries.accounts)));
|
||||
dispatch(importFetchedStatuses(Object.values(entries.statuses)));
|
||||
|
||||
const statusesFromGroups = (Object.values(entries.statuses) as Status[]).filter((status) => !!status.group);
|
||||
dispatch(fetchGroupRelationships(statusesFromGroups.map((status: any) => status.group?.id)));
|
||||
|
||||
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore));
|
||||
fetchRelatedRelationships(dispatch, response.data);
|
||||
done();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import api from '../api';
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts, importFetchedStatuses } from './importer';
|
||||
|
@ -83,7 +83,9 @@ const submitSearch = (filter?: SearchFilter) =>
|
|||
dispatch(importFetchedStatuses(response.data.statuses));
|
||||
}
|
||||
|
||||
dispatch(fetchSearchSuccess(response.data, value, type));
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(fetchSearchSuccess(response.data, value, type, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.accounts.map((item: APIEntity) => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(fetchSearchFail(error));
|
||||
|
@ -95,11 +97,12 @@ const fetchSearchRequest = (value: string) => ({
|
|||
value,
|
||||
});
|
||||
|
||||
const fetchSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter) => ({
|
||||
const fetchSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter, next: string | null) => ({
|
||||
type: SEARCH_FETCH_SUCCESS,
|
||||
results,
|
||||
searchTerm,
|
||||
searchType,
|
||||
next,
|
||||
});
|
||||
|
||||
const fetchSearchFail = (error: AxiosError) => ({
|
||||
|
@ -125,17 +128,26 @@ const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: (
|
|||
|
||||
dispatch(expandSearchRequest(type));
|
||||
|
||||
const params: Record<string, any> = {
|
||||
q: value,
|
||||
type,
|
||||
offset,
|
||||
};
|
||||
let url = getState().search.next as string;
|
||||
let params: Record<string, any> = {};
|
||||
|
||||
if (accountId) params.account_id = accountId;
|
||||
// if no URL was extracted from the Link header,
|
||||
// fall back on querying with the offset
|
||||
if (!url) {
|
||||
url = '/api/v2/search';
|
||||
params = {
|
||||
q: value,
|
||||
type,
|
||||
offset,
|
||||
};
|
||||
if (accountId) params.account_id = accountId;
|
||||
}
|
||||
|
||||
api(getState).get('/api/v2/search', {
|
||||
api(getState).get(url, {
|
||||
params,
|
||||
}).then(({ data }) => {
|
||||
}).then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (data.accounts) {
|
||||
dispatch(importFetchedAccounts(data.accounts));
|
||||
}
|
||||
|
@ -144,7 +156,9 @@ const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: (
|
|||
dispatch(importFetchedStatuses(data.statuses));
|
||||
}
|
||||
|
||||
dispatch(expandSearchSuccess(data, value, type));
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(expandSearchSuccess(data, value, type, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(data.accounts.map((item: APIEntity) => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(expandSearchFail(error));
|
||||
|
@ -156,11 +170,12 @@ const expandSearchRequest = (searchType: SearchFilter) => ({
|
|||
searchType,
|
||||
});
|
||||
|
||||
const expandSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter) => ({
|
||||
const expandSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter, next: string | null) => ({
|
||||
type: SEARCH_EXPAND_SUCCESS,
|
||||
results,
|
||||
searchTerm,
|
||||
searchType,
|
||||
next,
|
||||
});
|
||||
|
||||
const expandSearchFail = (error: AxiosError) => ({
|
||||
|
|
|
@ -6,6 +6,7 @@ import { shouldFilter } from 'soapbox/utils/timelines';
|
|||
|
||||
import api, { getNextLink, getPrevLink } from '../api';
|
||||
|
||||
import { fetchGroupRelationships } from './groups';
|
||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
@ -177,6 +178,10 @@ const expandTimeline = (timelineId: string, path: string, params: Record<string,
|
|||
|
||||
return api(getState).get(path, { params }).then(response => {
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
|
||||
const statusesFromGroups = (response.data as Status[]).filter((status) => !!status.group);
|
||||
dispatch(fetchGroupRelationships(statusesFromGroups.map((status: any) => status.group?.id)));
|
||||
|
||||
dispatch(expandTimelineSuccess(
|
||||
timelineId,
|
||||
response.data,
|
||||
|
@ -221,29 +226,29 @@ const expandHomeTimeline = ({ url, accountId, maxId }: ExpandHomeTimelineOpts =
|
|||
return expandTimeline('home', endpoint, params, done);
|
||||
};
|
||||
|
||||
const expandPublicTimeline = ({ maxId, onlyMedia }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
const expandPublicTimeline = ({ url, maxId, onlyMedia }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`public${onlyMedia ? ':media' : ''}`, url || '/api/v1/timelines/public', url ? {} : { max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
|
||||
const expandRemoteTimeline = (instance: string, { maxId, onlyMedia }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`remote${onlyMedia ? ':media' : ''}:${instance}`, '/api/v1/timelines/public', { local: false, instance: instance, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
const expandRemoteTimeline = (instance: string, { url, maxId, onlyMedia }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`remote${onlyMedia ? ':media' : ''}:${instance}`, url || '/api/v1/timelines/public', url ? {} : { local: false, instance: instance, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
|
||||
const expandCommunityTimeline = ({ maxId, onlyMedia }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
const expandCommunityTimeline = ({ url, maxId, onlyMedia }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`community${onlyMedia ? ':media' : ''}`, url || '/api/v1/timelines/public', url ? {} : { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
|
||||
const expandDirectTimeline = ({ maxId }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
|
||||
const expandDirectTimeline = ({ url, maxId }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline('direct', url || '/api/v1/timelines/direct', url ? {} : { max_id: maxId }, done);
|
||||
|
||||
const expandAccountTimeline = (accountId: string, { maxId, withReplies }: Record<string, any> = {}) =>
|
||||
expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId, with_muted: true });
|
||||
const expandAccountTimeline = (accountId: string, { url, maxId, withReplies }: Record<string, any> = {}) =>
|
||||
expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, url || `/api/v1/accounts/${accountId}/statuses`, url ? {} : { exclude_replies: !withReplies, max_id: maxId, with_muted: true });
|
||||
|
||||
const expandAccountFeaturedTimeline = (accountId: string) =>
|
||||
expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, with_muted: true });
|
||||
|
||||
const expandAccountMediaTimeline = (accountId: string | number, { maxId }: Record<string, any> = {}) =>
|
||||
expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40, with_muted: true });
|
||||
const expandAccountMediaTimeline = (accountId: string | number, { url, maxId }: Record<string, any> = {}) =>
|
||||
expandTimeline(`account:${accountId}:media`, url || `/api/v1/accounts/${accountId}/statuses`, url ? {} : { max_id: maxId, only_media: true, limit: 40, with_muted: true });
|
||||
|
||||
const expandListTimeline = (id: string, { maxId }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
||||
const expandListTimeline = (id: string, { url, maxId }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`list:${id}`, url || `/api/v1/timelines/list/${id}`, url ? {} : { max_id: maxId }, done);
|
||||
|
||||
const expandGroupTimeline = (id: string, { maxId }: Record<string, any> = {}, done = noOp) =>
|
||||
expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { max_id: maxId }, done);
|
||||
|
@ -257,8 +262,8 @@ const expandGroupTimelineFromTag = (id: string, tagName: string, { maxId }: Reco
|
|||
const expandGroupMediaTimeline = (id: string | number, { maxId }: Record<string, any> = {}) =>
|
||||
expandTimeline(`group:${id}:media`, `/api/v1/timelines/group/${id}`, { max_id: maxId, only_media: true, limit: 40, with_muted: true });
|
||||
|
||||
const expandHashtagTimeline = (hashtag: string, { maxId, tags }: Record<string, any> = {}, done = noOp) => {
|
||||
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||
const expandHashtagTimeline = (hashtag: string, { url, maxId, tags }: Record<string, any> = {}, done = noOp) => {
|
||||
return expandTimeline(`hashtag:${hashtag}`, url || `/api/v1/timelines/tag/${hashtag}`, url ? {} : {
|
||||
max_id: maxId,
|
||||
any: parseTags(tags, 'any'),
|
||||
all: parseTags(tags, 'all'),
|
||||
|
|
|
@ -3,15 +3,17 @@ import { FormattedMessage } from 'react-intl';
|
|||
|
||||
import ThumbNavigationLink from 'soapbox/components/thumb-navigation-link';
|
||||
import { useStatContext } from 'soapbox/contexts/stat-context';
|
||||
import { useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks';
|
||||
import { useAppSelector, useFeatures, useGroupsPath, useOwnAccount } from 'soapbox/hooks';
|
||||
|
||||
const ThumbNavigation: React.FC = (): JSX.Element => {
|
||||
const account = useOwnAccount();
|
||||
const features = useFeatures();
|
||||
const groupsPath = useGroupsPath();
|
||||
|
||||
const { unreadChatsCount } = useStatContext();
|
||||
|
||||
const notificationCount = useAppSelector((state) => state.notifications.unread);
|
||||
const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count());
|
||||
const features = useFeatures();
|
||||
|
||||
/** Conditionally render the supported messages link */
|
||||
const renderMessagesLink = (): React.ReactNode => {
|
||||
|
@ -51,6 +53,15 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
|
|||
exact
|
||||
/>
|
||||
|
||||
{features.groups && (
|
||||
<ThumbNavigationLink
|
||||
src={require('@tabler/icons/circles.svg')}
|
||||
text={<FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />}
|
||||
to={groupsPath}
|
||||
exact
|
||||
/>
|
||||
)}
|
||||
|
||||
<ThumbNavigationLink
|
||||
src={require('@tabler/icons/search.svg')}
|
||||
text={<FormattedMessage id='navigation.search' defaultMessage='Search' />}
|
||||
|
|
|
@ -64,6 +64,7 @@ const AccountGallery = () => {
|
|||
const attachments: ImmutableList<Attachment> = useAppSelector((state) => getAccountGallery(state, accountId as string));
|
||||
const isLoading = useAppSelector((state) => state.timelines.get(`account:${accountId}:media`)?.isLoading);
|
||||
const hasMore = useAppSelector((state) => state.timelines.get(`account:${accountId}:media`)?.hasMore);
|
||||
const next = useAppSelector(state => state.timelines.get(`account:${accountId}:media`)?.next);
|
||||
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
@ -75,7 +76,7 @@ const AccountGallery = () => {
|
|||
|
||||
const handleLoadMore = (maxId: string | null) => {
|
||||
if (accountId && accountId !== -1) {
|
||||
dispatch(expandAccountMediaTimeline(accountId, { maxId }));
|
||||
dispatch(expandAccountMediaTimeline(accountId, { url: next, maxId }));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ const AccountTimeline: React.FC<IAccountTimeline> = ({ params, withReplies = fal
|
|||
const patronEnabled = soapboxConfig.getIn(['extensions', 'patron', 'enabled']) === true;
|
||||
const isLoading = useAppSelector(state => state.getIn(['timelines', `account:${path}`, 'isLoading']) === true);
|
||||
const hasMore = useAppSelector(state => state.getIn(['timelines', `account:${path}`, 'hasMore']) === true);
|
||||
const next = useAppSelector(state => state.timelines.get(`account:${path}`)?.next);
|
||||
|
||||
const accountUsername = account?.username || params.username;
|
||||
|
||||
|
@ -69,7 +70,7 @@ const AccountTimeline: React.FC<IAccountTimeline> = ({ params, withReplies = fal
|
|||
|
||||
const handleLoadMore = (maxId: string) => {
|
||||
if (account) {
|
||||
dispatch(expandAccountTimeline(account.id, { maxId, withReplies }));
|
||||
dispatch(expandAccountTimeline(account.id, { url: next, maxId, withReplies }));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import { connectCommunityStream } from 'soapbox/actions/streaming';
|
|||
import { expandCommunityTimeline } from 'soapbox/actions/timelines';
|
||||
import PullToRefresh from 'soapbox/components/pull-to-refresh';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useSettings } from 'soapbox/hooks';
|
||||
import { useAppSelector, useAppDispatch, useSettings } from 'soapbox/hooks';
|
||||
|
||||
import Timeline from '../ui/components/timeline';
|
||||
|
||||
|
@ -19,11 +19,12 @@ const CommunityTimeline = () => {
|
|||
|
||||
const settings = useSettings();
|
||||
const onlyMedia = settings.getIn(['community', 'other', 'onlyMedia']);
|
||||
const next = useAppSelector(state => state.timelines.get('community')?.next);
|
||||
|
||||
const timelineId = 'community';
|
||||
|
||||
const handleLoadMore = (maxId: string) => {
|
||||
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
|
||||
dispatch(expandCommunityTimeline({ url: next, maxId, onlyMedia }));
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
|
|
|
@ -6,7 +6,7 @@ import { connectDirectStream } from 'soapbox/actions/streaming';
|
|||
import { expandDirectTimeline } from 'soapbox/actions/timelines';
|
||||
import AccountSearch from 'soapbox/components/account-search';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
import Timeline from '../ui/components/timeline';
|
||||
|
||||
|
@ -18,6 +18,7 @@ const messages = defineMessages({
|
|||
const DirectTimeline = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const next = useAppSelector(state => state.timelines.get('direct')?.next);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(expandDirectTimeline());
|
||||
|
@ -33,7 +34,7 @@ const DirectTimeline = () => {
|
|||
};
|
||||
|
||||
const handleLoadMore = (maxId: string) => {
|
||||
dispatch(expandDirectTimeline({ maxId }));
|
||||
dispatch(expandDirectTimeline({ url: next, maxId }));
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -39,6 +39,7 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
|||
const dispatch = useAppDispatch();
|
||||
const disconnects = useRef<(() => void)[]>([]);
|
||||
const tag = useAppSelector((state) => state.tags.get(id));
|
||||
const next = useAppSelector(state => state.timelines.get(`hashtag:${id}`)?.next);
|
||||
|
||||
// Mastodon supports displaying results from multiple hashtags.
|
||||
// https://github.com/mastodon/mastodon/issues/6359
|
||||
|
@ -89,7 +90,7 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
|||
};
|
||||
|
||||
const handleLoadMore = (maxId: string) => {
|
||||
dispatch(expandHashtagTimeline(id, { maxId, tags }));
|
||||
dispatch(expandHashtagTimeline(id, { url: next, maxId, tags }));
|
||||
};
|
||||
|
||||
const handleFollow = () => {
|
||||
|
|
|
@ -17,6 +17,7 @@ const ListTimeline: React.FC = () => {
|
|||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
const list = useAppSelector((state) => state.lists.get(id));
|
||||
const next = useAppSelector(state => state.timelines.get(`list:${id}`)?.next);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchList(id));
|
||||
|
@ -30,7 +31,7 @@ const ListTimeline: React.FC = () => {
|
|||
}, [id]);
|
||||
|
||||
const handleLoadMore = (maxId: string) => {
|
||||
dispatch(expandListTimeline(id, { maxId }));
|
||||
dispatch(expandListTimeline(id, { url: next, maxId }));
|
||||
};
|
||||
|
||||
const handleEditClick = () => {
|
||||
|
|
|
@ -7,7 +7,7 @@ import { connectPublicStream } from 'soapbox/actions/streaming';
|
|||
import { expandPublicTimeline } from 'soapbox/actions/timelines';
|
||||
import PullToRefresh from 'soapbox/components/pull-to-refresh';
|
||||
import { Accordion, Column } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useInstance, useSettings } from 'soapbox/hooks';
|
||||
import { useAppSelector, useAppDispatch, useInstance, useSettings } from 'soapbox/hooks';
|
||||
|
||||
import PinnedHostsPicker from '../remote-timeline/components/pinned-hosts-picker';
|
||||
import Timeline from '../ui/components/timeline';
|
||||
|
@ -24,6 +24,7 @@ const CommunityTimeline = () => {
|
|||
const instance = useInstance();
|
||||
const settings = useSettings();
|
||||
const onlyMedia = settings.getIn(['public', 'other', 'onlyMedia']);
|
||||
const next = useAppSelector(state => state.timelines.get('public')?.next);
|
||||
|
||||
const timelineId = 'public';
|
||||
|
||||
|
@ -39,7 +40,7 @@ const CommunityTimeline = () => {
|
|||
};
|
||||
|
||||
const handleLoadMore = (maxId: string) => {
|
||||
dispatch(expandPublicTimeline({ maxId, onlyMedia }));
|
||||
dispatch(expandPublicTimeline({ url: next, maxId, onlyMedia }));
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
|
|
|
@ -6,7 +6,7 @@ import { connectRemoteStream } from 'soapbox/actions/streaming';
|
|||
import { expandRemoteTimeline } from 'soapbox/actions/timelines';
|
||||
import IconButton from 'soapbox/components/icon-button';
|
||||
import { Column, HStack, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useSettings } from 'soapbox/hooks';
|
||||
import { useAppSelector, useAppDispatch, useSettings } from 'soapbox/hooks';
|
||||
|
||||
import Timeline from '../ui/components/timeline';
|
||||
|
||||
|
@ -30,6 +30,7 @@ const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
|
|||
|
||||
const timelineId = 'remote';
|
||||
const onlyMedia = !!settings.getIn(['remote', 'other', 'onlyMedia']);
|
||||
const next = useAppSelector(state => state.timelines.get('remote')?.next);
|
||||
|
||||
const pinned: boolean = (settings.getIn(['remote_timeline', 'pinnedHosts']) as any).includes(instance);
|
||||
|
||||
|
@ -44,7 +45,7 @@ const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
|
|||
};
|
||||
|
||||
const handleLoadMore = (maxId: string) => {
|
||||
dispatch(expandRemoteTimeline(instance, { maxId, onlyMedia }));
|
||||
dispatch(expandRemoteTimeline(instance, { url: next, maxId, onlyMedia }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -38,6 +38,7 @@ import HomePage from 'soapbox/pages/home-page';
|
|||
import ManageGroupsPage from 'soapbox/pages/manage-groups-page';
|
||||
import ProfilePage from 'soapbox/pages/profile-page';
|
||||
import RemoteInstancePage from 'soapbox/pages/remote-instance-page';
|
||||
import SearchPage from 'soapbox/pages/search-page';
|
||||
import StatusPage from 'soapbox/pages/status-page';
|
||||
import { usePendingPolicy } from 'soapbox/queries/policies';
|
||||
import { getAccessToken, getVapidKey } from 'soapbox/utils/auth';
|
||||
|
@ -275,7 +276,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
|||
|
||||
<WrappedRoute path='/notifications' page={DefaultPage} component={Notifications} content={children} />
|
||||
|
||||
<WrappedRoute path='/search' page={DefaultPage} component={Search} content={children} />
|
||||
<WrappedRoute path='/search' page={SearchPage} component={Search} content={children} />
|
||||
{features.suggestions && <WrappedRoute path='/suggestions' publicRoute page={DefaultPage} component={FollowRecommendations} content={children} />}
|
||||
{features.profileDirectory && <WrappedRoute path='/directory' publicRoute page={DefaultPage} component={Directory} content={children} />}
|
||||
{features.events && <WrappedRoute path='/events' page={EventsPage} component={Events} content={children} />}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import React from 'react';
|
||||
|
||||
import LinkFooter from 'soapbox/features/ui/components/link-footer';
|
||||
import BundleContainer from 'soapbox/features/ui/containers/bundle-container';
|
||||
import {
|
||||
WhoToFollowPanel,
|
||||
TrendsPanel,
|
||||
SignUpPanel,
|
||||
CtaBanner,
|
||||
SuggestedGroupsPanel,
|
||||
} from 'soapbox/features/ui/util/async-components';
|
||||
import { useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||
|
||||
import { Layout } from '../components/ui';
|
||||
|
||||
interface ISearchPage {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const SearchPage: React.FC<ISearchPage> = ({ children }) => {
|
||||
const me = useAppSelector(state => state.me);
|
||||
const features = useFeatures();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Layout.Main>
|
||||
{children}
|
||||
|
||||
{!me && (
|
||||
<BundleContainer fetchComponent={CtaBanner}>
|
||||
{Component => <Component key='cta-banner' />}
|
||||
</BundleContainer>
|
||||
)}
|
||||
</Layout.Main>
|
||||
|
||||
<Layout.Aside>
|
||||
{!me && (
|
||||
<BundleContainer fetchComponent={SignUpPanel}>
|
||||
{Component => <Component key='sign-up-panel' />}
|
||||
</BundleContainer>
|
||||
)}
|
||||
|
||||
{features.trends && (
|
||||
<BundleContainer fetchComponent={TrendsPanel}>
|
||||
{Component => <Component limit={5} key='trends-panel' />}
|
||||
</BundleContainer>
|
||||
)}
|
||||
|
||||
{me && features.suggestions && (
|
||||
<BundleContainer fetchComponent={WhoToFollowPanel}>
|
||||
{Component => <Component limit={3} key='wtf-panel' />}
|
||||
</BundleContainer>
|
||||
)}
|
||||
|
||||
{features.groups && (
|
||||
<BundleContainer fetchComponent={SuggestedGroupsPanel}>
|
||||
{Component => <Component key='suggested-groups-panel' />}
|
||||
</BundleContainer>
|
||||
)}
|
||||
|
||||
<LinkFooter key='link-footer' />
|
||||
</Layout.Aside>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchPage;
|
|
@ -47,6 +47,7 @@ const ReducerRecord = ImmutableRecord({
|
|||
results: ResultsRecord(),
|
||||
filter: 'accounts' as SearchFilter,
|
||||
accountId: null as string | null,
|
||||
next: null as string | null,
|
||||
});
|
||||
|
||||
type State = ReturnType<typeof ReducerRecord>;
|
||||
|
@ -57,7 +58,7 @@ const toIds = (items: APIEntities = []) => {
|
|||
return ImmutableOrderedSet(items.map(item => item.id));
|
||||
};
|
||||
|
||||
const importResults = (state: State, results: APIEntity, searchTerm: string, searchType: SearchFilter) => {
|
||||
const importResults = (state: State, results: APIEntity, searchTerm: string, searchType: SearchFilter, next: string | null) => {
|
||||
return state.withMutations(state => {
|
||||
if (state.value === searchTerm && state.filter === searchType) {
|
||||
state.set('results', ResultsRecord({
|
||||
|
@ -76,15 +77,17 @@ const importResults = (state: State, results: APIEntity, searchTerm: string, sea
|
|||
}));
|
||||
|
||||
state.set('submitted', true);
|
||||
state.set('next', next);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const paginateResults = (state: State, searchType: SearchFilter, results: APIEntity, searchTerm: string) => {
|
||||
const paginateResults = (state: State, searchType: SearchFilter, results: APIEntity, searchTerm: string, next: string | null) => {
|
||||
return state.withMutations(state => {
|
||||
if (state.value === searchTerm) {
|
||||
state.setIn(['results', `${searchType}HasMore`], results[searchType].length >= 20);
|
||||
state.setIn(['results', `${searchType}Loaded`], true);
|
||||
state.set('next', next);
|
||||
state.updateIn(['results', searchType], items => {
|
||||
const data = results[searchType];
|
||||
// Hashtags are a list of maps. Others are IDs.
|
||||
|
@ -129,13 +132,13 @@ export default function search(state = ReducerRecord(), action: AnyAction) {
|
|||
case SEARCH_FETCH_REQUEST:
|
||||
return handleSubmitted(state, action.value);
|
||||
case SEARCH_FETCH_SUCCESS:
|
||||
return importResults(state, action.results, action.searchTerm, action.searchType);
|
||||
return importResults(state, action.results, action.searchTerm, action.searchType, action.next);
|
||||
case SEARCH_FILTER_SET:
|
||||
return state.set('filter', action.value);
|
||||
case SEARCH_EXPAND_REQUEST:
|
||||
return state.setIn(['results', `${action.searchType}Loaded`], false);
|
||||
case SEARCH_EXPAND_SUCCESS:
|
||||
return paginateResults(state, action.searchType, action.results, action.searchTerm);
|
||||
return paginateResults(state, action.searchType, action.results, action.searchTerm, action.next);
|
||||
case SEARCH_ACCOUNT_SET:
|
||||
if (!action.accountId) return state.merge({
|
||||
results: ResultsRecord(),
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import type { Account } from 'soapbox/schemas';
|
||||
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
||||
|
||||
const getDomainFromURL = (account: Pick<AccountEntity, 'url'>): string => {
|
||||
const getDomainFromURL = (account: Pick<Account, 'url'>): string => {
|
||||
try {
|
||||
const url = account.url;
|
||||
return new URL(url).host;
|
||||
|
@ -10,12 +9,12 @@ const getDomainFromURL = (account: Pick<AccountEntity, 'url'>): string => {
|
|||
}
|
||||
};
|
||||
|
||||
export const getDomain = (account: Pick<AccountEntity, 'acct' | 'url'>): string => {
|
||||
export const getDomain = (account: Pick<Account, 'acct' | 'url'>): string => {
|
||||
const domain = account.acct.split('@')[1];
|
||||
return domain ? domain : getDomainFromURL(account);
|
||||
};
|
||||
|
||||
export const getBaseURL = (account: AccountEntity): string => {
|
||||
export const getBaseURL = (account: Pick<Account, 'url'>): string => {
|
||||
try {
|
||||
return new URL(account.url).origin;
|
||||
} catch {
|
||||
|
@ -27,12 +26,12 @@ export const getAcct = (account: Pick<Account, 'fqn' | 'acct'>, displayFqn: bool
|
|||
displayFqn === true ? account.fqn : account.acct
|
||||
);
|
||||
|
||||
export const isLocal = (account: AccountEntity | Account): boolean => {
|
||||
export const isLocal = (account: Pick<Account, 'acct'>): boolean => {
|
||||
const domain: string = account.acct.split('@')[1];
|
||||
return domain === undefined ? true : false;
|
||||
};
|
||||
|
||||
export const isRemote = (account: AccountEntity): boolean => !isLocal(account);
|
||||
export const isRemote = (account: Pick<Account, 'acct'>): boolean => !isLocal(account);
|
||||
|
||||
/** Default header filenames from various backends */
|
||||
const DEFAULT_HEADERS = [
|
||||
|
|
|
@ -4,7 +4,7 @@ import type { Ad } from 'soapbox/schemas';
|
|||
const AD_EXPIRY_THRESHOLD = 5 * 60 * 1000;
|
||||
|
||||
/** Whether the ad is expired or about to expire. */
|
||||
const isExpired = (ad: Ad, threshold = AD_EXPIRY_THRESHOLD): boolean => {
|
||||
const isExpired = (ad: Pick<Ad, 'expires_at'>, threshold = AD_EXPIRY_THRESHOLD): boolean => {
|
||||
if (ad.expires_at) {
|
||||
const now = new Date();
|
||||
return now.getTime() > (new Date(ad.expires_at).getTime() - threshold);
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { isIntegerId } from 'soapbox/utils/numbers';
|
||||
|
||||
import type { IntlShape } from 'react-intl';
|
||||
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
||||
import type { Status } from 'soapbox/types/entities';
|
||||
|
||||
/** Get the initial visibility of media attachments from user settings. */
|
||||
export const defaultMediaVisibility = (status: StatusEntity | undefined | null, displayMedia: string): boolean => {
|
||||
export const defaultMediaVisibility = (
|
||||
status: Pick<Status, 'reblog' | 'visibility' | 'sensitive'> | undefined | null,
|
||||
displayMedia: string,
|
||||
): boolean => {
|
||||
if (!status) return false;
|
||||
|
||||
if (status.reblog && typeof status.reblog === 'object') {
|
||||
|
@ -21,7 +24,7 @@ export const defaultMediaVisibility = (status: StatusEntity | undefined | null,
|
|||
};
|
||||
|
||||
/** Grab the first external link from a status. */
|
||||
export const getFirstExternalLink = (status: StatusEntity): HTMLAnchorElement | null => {
|
||||
export const getFirstExternalLink = (status: Pick<Status, 'content'>): HTMLAnchorElement | null => {
|
||||
try {
|
||||
// Pulled from Pleroma's media parser
|
||||
const selector = 'a:not(.mention,.hashtag,.attachment,[rel~="tag"])';
|
||||
|
@ -34,18 +37,22 @@ export const getFirstExternalLink = (status: StatusEntity): HTMLAnchorElement |
|
|||
};
|
||||
|
||||
/** Whether the status is expected to have a Card after it loads. */
|
||||
export const shouldHaveCard = (status: StatusEntity): boolean => {
|
||||
export const shouldHaveCard = (status: Pick<Status, 'content'>): boolean => {
|
||||
return Boolean(getFirstExternalLink(status));
|
||||
};
|
||||
|
||||
/** Whether the media IDs on this status have integer IDs (opposed to FlakeIds). */
|
||||
// https://gitlab.com/soapbox-pub/soapbox/-/merge_requests/1087
|
||||
export const hasIntegerMediaIds = (status: StatusEntity): boolean => {
|
||||
export const hasIntegerMediaIds = (status: Pick<Status, 'media_attachments'>): boolean => {
|
||||
return status.media_attachments.some(({ id }) => isIntegerId(id));
|
||||
};
|
||||
|
||||
/** Sanitize status text for use with screen readers. */
|
||||
export const textForScreenReader = (intl: IntlShape, status: StatusEntity, rebloggedByText?: string): string => {
|
||||
export const textForScreenReader = (
|
||||
intl: IntlShape,
|
||||
status: Pick<Status, 'account' | 'spoiler_text' | 'hidden' | 'search_index' | 'created_at'>,
|
||||
rebloggedByText?: string,
|
||||
): string => {
|
||||
const { account } = status;
|
||||
if (!account || typeof account !== 'object') return '';
|
||||
|
||||
|
@ -55,7 +62,7 @@ export const textForScreenReader = (intl: IntlShape, status: StatusEntity, reblo
|
|||
displayName.length === 0 ? account.acct.split('@')[0] : displayName,
|
||||
status.spoiler_text && status.hidden ? status.spoiler_text : status.search_index.slice(status.spoiler_text.length),
|
||||
intl.formatDate(status.created_at, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
|
||||
status.getIn(['account', 'acct']),
|
||||
account.acct,
|
||||
];
|
||||
|
||||
if (rebloggedByText) {
|
||||
|
@ -68,12 +75,12 @@ export const textForScreenReader = (intl: IntlShape, status: StatusEntity, reblo
|
|||
/** Get reblogged status if any, otherwise return the original status. */
|
||||
// @ts-ignore The type seems right, but TS doesn't like it.
|
||||
export const getActualStatus: {
|
||||
(status: StatusEntity): StatusEntity
|
||||
<T extends Pick<Status, 'reblog'>>(status: T): T
|
||||
(status: undefined): undefined
|
||||
(status: null): null
|
||||
} = (status) => {
|
||||
} = <T extends Pick<Status, 'reblog'>>(status: T | null | undefined) => {
|
||||
if (status?.reblog && typeof status?.reblog === 'object') {
|
||||
return status.reblog as StatusEntity;
|
||||
return status.reblog as Status;
|
||||
} else {
|
||||
return status;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue