Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-08-23 01:21:30 +02:00
parent 1549043c0a
commit 6f1c11b39f
15 changed files with 36 additions and 183 deletions

View file

@ -50,7 +50,6 @@
"@fontsource/noto-sans-javanese": "^5.0.16",
"@fontsource/roboto-mono": "^5.0.0",
"@fontsource/tajawal": "^5.0.8",
"@gamestdio/websocket": "^0.3.2",
"@lexical/clipboard": "^0.14.5",
"@lexical/code": "^0.14.5",
"@lexical/hashtag": "^0.14.5",

View file

@ -10,7 +10,6 @@ import { getClient, type PlfeResponse } from '../api';
import {
importFetchedAccount,
importFetchedAccounts,
importErrorWhileFetchingAccountByUsername,
} from './importer';
import type { Map as ImmutableMap } from 'immutable';
@ -168,7 +167,6 @@ const fetchAccountByUsername = (username: string, history?: History) =>
dispatch(fetchAccountSuccess(response));
}).catch(error => {
dispatch(fetchAccountFail(null, error));
dispatch(importErrorWhileFetchingAccountByUsername(username));
});
} else if (features.accountLookup) {
return dispatch(accountLookup(username)).then(account => {
@ -176,7 +174,6 @@ const fetchAccountByUsername = (username: string, history?: History) =>
dispatch(fetchAccountSuccess(account));
}).catch(error => {
dispatch(fetchAccountFail(null, error));
dispatch(importErrorWhileFetchingAccountByUsername(username));
maybeRedirectLogin(error, history);
});
} else {
@ -191,7 +188,6 @@ const fetchAccountByUsername = (username: string, history?: History) =>
}
}).catch(error => {
dispatch(fetchAccountFail(null, error));
dispatch(importErrorWhileFetchingAccountByUsername(username));
});
}
};

View file

@ -66,6 +66,14 @@ const expandDirectoryFail = (error: unknown) => ({
error,
});
type DirectoryAction =
| ReturnType<typeof fetchDirectoryRequest>
| ReturnType<typeof fetchDirectorySuccess>
| ReturnType<typeof fetchDirectoryFail>
| ReturnType<typeof expandDirectoryRequest>
| ReturnType<typeof expandDirectorySuccess>
| ReturnType<typeof expandDirectoryFail>;
export {
DIRECTORY_FETCH_REQUEST,
DIRECTORY_FETCH_SUCCESS,
@ -81,4 +89,5 @@ export {
expandDirectoryRequest,
expandDirectorySuccess,
expandDirectoryFail,
type DirectoryAction,
};

View file

@ -1,9 +1,8 @@
import { accountSchema, groupSchema, type Account as BaseAccount, type Group, type Poll, type Status as BaseStatus } from 'pl-api';
import { type Account as BaseAccount, type Group, type Poll, type Status as BaseStatus } from 'pl-api';
import { importEntities } from 'soapbox/entity-store/actions';
import { Entities } from 'soapbox/entity-store/entities';
import { normalizeAccount, normalizeGroup } from 'soapbox/normalizers';
import { filteredArray } from 'soapbox/schemas/utils';
import type { AppDispatch } from 'soapbox/store';
@ -12,14 +11,13 @@ const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
const STATUS_IMPORT = 'STATUS_IMPORT';
const STATUSES_IMPORT = 'STATUSES_IMPORT';
const POLLS_IMPORT = 'POLLS_IMPORT';
const ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP = 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP';
const importAccount = (data: BaseAccount) => importAccounts([data]);
const importAccounts = (data: Array<BaseAccount>) => (dispatch: AppDispatch) => {
dispatch({ type: ACCOUNTS_IMPORT, accounts: data });
try {
const accounts = filteredArray(accountSchema).parse(data).map(normalizeAccount);
const accounts = data.map(normalizeAccount);
dispatch(importEntities(accounts, Entities.ACCOUNTS));
} catch (e) {
//
@ -30,7 +28,7 @@ const importGroup = (data: Group) => importGroups([data]);
const importGroups = (data: Array<Group>) => (dispatch: AppDispatch) => {
try {
const groups = filteredArray(groupSchema).parse(data).map(normalizeGroup);
const groups = data.map(normalizeGroup);
dispatch(importEntities(groups, Entities.GROUPS));
} catch (e) {
//
@ -155,16 +153,12 @@ const importFetchedPoll = (poll: Poll) =>
dispatch(importPolls([poll]));
};
const importErrorWhileFetchingAccountByUsername = (username: string) =>
({ type: ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP, username });
export {
ACCOUNT_IMPORT,
ACCOUNTS_IMPORT,
STATUS_IMPORT,
STATUSES_IMPORT,
POLLS_IMPORT,
ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP,
importAccount,
importAccounts,
importGroup,
@ -177,5 +171,4 @@ export {
importFetchedStatus,
importFetchedStatuses,
importFetchedPoll,
importErrorWhileFetchingAccountByUsername,
};

View file

@ -130,7 +130,7 @@ const expandSearchRequest = (searchType: SearchFilter) => ({
searchType,
});
const expandSearchSuccess = (results: Search, searchTerm: string, searchType: SearchFilter) => ({
const expandSearchSuccess = (results: Search, searchTerm: string, searchType: Exclude<SearchFilter, 'links'>) => ({
type: SEARCH_EXPAND_SUCCESS,
results,
searchTerm,

View file

@ -22,7 +22,6 @@ const getSoapboxConfig = createSelector([
], (soapbox, features) => {
// Do some additional normalization with the state
return normalizeSoapboxConfig(soapbox).withMutations(soapboxConfig => {
// If displayFqn isn't set, infer it from federation
if (soapbox.get('displayFqn') === undefined) {
soapboxConfig.set('displayFqn', features.federating);

View file

@ -162,11 +162,7 @@ const fetchContext = (statusId: string, intl?: IntlShape) =>
} : undefined;
return getClient(getState()).statuses.getContext(statusId, params).then(context => {
if (Array.isArray(context)) {
// Mitra: returns a list of statuses
dispatch(importFetchedStatuses(context));
} else if (typeof context === 'object') {
// Standard Mastodon API returns a map with `ancestors` and `descendants`
if (typeof context === 'object') {
const { ancestors, descendants } = context;
const statuses = ancestors.concat(descendants);
dispatch(importFetchedStatuses(statuses));

View file

@ -25,7 +25,7 @@ const TIMELINE_INSERT = 'TIMELINE_INSERT' as const;
const MAX_QUEUED_ITEMS = 40;
const processTimelineUpdate = (timeline: string, status: BaseStatus, accept: ((status: BaseStatus) => boolean) | null = null) =>
const processTimelineUpdate = (timeline: string, status: BaseStatus) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const me = getState().me;
const ownStatus = status.account?.id === me;
@ -48,26 +48,19 @@ const processTimelineUpdate = (timeline: string, status: BaseStatus, accept: ((s
dispatch(importFetchedStatus(status));
if (shouldSkipQueue) {
dispatch(updateTimeline(timeline, status.id, accept));
dispatch(updateTimeline(timeline, status.id));
} else {
dispatch(updateTimelineQueue(timeline, status.id, accept));
dispatch(updateTimelineQueue(timeline, status.id));
}
};
const updateTimeline = (timeline: string, statusId: string, accept: ((status: BaseStatus) => boolean) | null) =>
(dispatch: AppDispatch) => {
// if (typeof accept === 'function' && !accept(status)) {
// return;
// }
const updateTimeline = (timeline: string, statusId: string) => ({
type: TIMELINE_UPDATE,
timeline,
statusId,
});
dispatch({
type: TIMELINE_UPDATE,
timeline,
statusId,
});
};
const updateTimelineQueue = (timeline: string, statusId: string, accept: ((status: BaseStatus) => boolean) | null) =>
const updateTimelineQueue = (timeline: string, statusId: string) =>
(dispatch: AppDispatch) => {
// if (typeof accept === 'function' && !accept(status)) {
// return;

View file

@ -1,5 +1,4 @@
import { useInfiniteQuery } from '@tanstack/react-query';
import { PaginatedResponse, type Account as BaseAccount } from 'pl-api';
import { Entities } from 'soapbox/entity-store/entities';
import { useClient } from 'soapbox/hooks';
@ -8,13 +7,10 @@ import { flattenPages } from 'soapbox/utils/queries';
import { useRelationships } from './useRelationships';
import type { PaginatedResponse, Account as BaseAccount } from 'pl-api';
import type { EntityFn } from 'soapbox/entity-store/hooks/types';
interface useAccountListOpts {
enabled?: boolean;
}
const useAccountList = (listKey: string[], entityFn: EntityFn<void>, opts: useAccountListOpts = {}) => {
const useAccountList = (listKey: string[], entityFn: EntityFn<void>) => {
const getAccounts = async (pageParam?: Pick<PaginatedResponse<BaseAccount>, 'next'>) => {
const response = await (pageParam?.next ? pageParam.next() : entityFn()) as PaginatedResponse<BaseAccount>;
@ -63,7 +59,6 @@ const useFollowing = (accountId: string | undefined) => {
return useAccountList(
[accountId!, 'following'],
() => client.accounts.getAccountFollowing(accountId!),
{ enabled: !!accountId },
);
};
@ -73,7 +68,6 @@ const useFollowers = (accountId: string | undefined) => {
return useAccountList(
[accountId!, 'followers'],
() => client.accounts.getAccountFollowers(accountId!),
{ enabled: !!accountId },
);
};

View file

@ -1,5 +1,4 @@
import clsx from 'clsx';
import { MediaAttachment } from 'pl-api';
import React, { useState, useRef, useLayoutEffect } from 'react';
import Blurhash from 'soapbox/components/blurhash';
@ -13,6 +12,7 @@ import { isIOS } from '../is-mobile';
import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media-aspect-ratio';
import type { Property } from 'csstype';
import type { MediaAttachment } from 'pl-api';
const ATTACHMENT_LIMIT = 4;
const MAX_FILENAME_LENGTH = 45;

View file

@ -67,8 +67,6 @@ const ModalRoot: React.FC = () => {
}));
const onClickClose = (type?: ModalType) => {
if (!type) return;
switch (type) {
case 'COMPOSE':
dispatch(cancelReplyCompose());

View file

@ -21,7 +21,6 @@ import {
} from '../actions/search';
import type { Search, Tag } from 'pl-api';
import type { APIEntity } from 'soapbox/types/entities';
const ResultsRecord = ImmutableRecord({
accounts: ImmutableOrderedSet<string>(),
@ -48,10 +47,9 @@ const ReducerRecord = ImmutableRecord({
});
type State = ReturnType<typeof ReducerRecord>;
type APIEntities = Array<APIEntity>;
type SearchFilter = 'accounts' | 'statuses' | 'groups' | 'hashtags' | 'links';
const toIds = (items: APIEntities = []) => ImmutableOrderedSet(items.map(item => item.id));
const toIds = (items: Array<{ id: string }> = []) => ImmutableOrderedSet(items.map(item => item.id));
const importResults = (state: State, results: Search, searchTerm: string, searchType: SearchFilter) =>
state.withMutations(state => {
@ -75,7 +73,7 @@ const importResults = (state: State, results: Search, searchTerm: string, search
}
});
const paginateResults = (state: State, searchType: SearchFilter, results: APIEntity, searchTerm: string) =>
const paginateResults = (state: State, searchType: Exclude<SearchFilter, 'links'>, results: Search, searchTerm: string) =>
state.withMutations(state => {
if (state.submittedValue === searchTerm) {
state.setIn(['results', `${searchType}HasMore`], results[searchType].length >= 20);
@ -84,9 +82,9 @@ const paginateResults = (state: State, searchType: SearchFilter, results: APIEnt
const data = results[searchType];
// Hashtags are a list of maps. Others are IDs.
if (searchType === 'hashtags') {
return (items as ImmutableOrderedSet<Tag>).concat(data);
return (items as ImmutableOrderedSet<Tag>).concat(data as Search['hashtags']);
} else {
return (items as ImmutableOrderedSet<string>).concat(toIds(data));
return (items as ImmutableOrderedSet<string>).concat(toIds(data as Search['accounts']));
}
});
}

View file

@ -24,6 +24,7 @@ import {
DIRECTORY_EXPAND_REQUEST,
DIRECTORY_EXPAND_SUCCESS,
DIRECTORY_EXPAND_FAIL,
DirectoryAction,
} from 'soapbox/actions/directory';
import {
EVENT_PARTICIPATIONS_EXPAND_SUCCESS,
@ -116,13 +117,13 @@ type Items = ImmutableOrderedSet<string>;
type NestedListPath = ['followers' | 'following' | 'reblogged_by' | 'favourited_by' | 'disliked_by' | 'reactions' | 'pinned' | 'birthday_reminders' | 'familiar_followers' | 'event_participations' | 'event_participation_requests' | 'membership_requests' | 'group_blocks', string];
type ListPath = ['follow_requests' | 'mutes' | 'directory'];
const normalizeList = (state: State, path: NestedListPath | ListPath, accounts: APIEntity[], next?: (() => any) | null) =>
const normalizeList = (state: State, path: NestedListPath | ListPath, accounts: Array<Pick<Account, 'id'>>, next?: (() => any) | null) =>
state.setIn(path, ListRecord({
next,
items: ImmutableOrderedSet(accounts.map(item => item.id)),
}));
const appendToList = (state: State, path: NestedListPath | ListPath, accounts: APIEntity[], next: (() => any) | null) =>
const appendToList = (state: State, path: NestedListPath | ListPath, accounts: Array<Pick<Account, 'id'>>, next: (() => any) | null) =>
state.updateIn(path, map => (map as List)
.set('next', next)
.set('isLoading', false)
@ -139,7 +140,7 @@ const normalizeFollowRequest = (state: State, notification: Notification) =>
ImmutableOrderedSet([notification.account.id]).union(list as Items),
);
const userLists = (state = ReducerRecord(), action: AnyAction) => {
const userLists = (state = ReducerRecord(), action: DirectoryAction | AnyAction) => {
switch (action.type) {
case FOLLOWERS_FETCH_SUCCESS:
return normalizeList(state, ['followers', action.accountId], action.accounts, action.next);
@ -176,9 +177,9 @@ const userLists = (state = ReducerRecord(), action: AnyAction) => {
case FOLLOW_REQUEST_REJECT_SUCCESS:
return removeFromList(state, ['follow_requests'], action.accountId);
case DIRECTORY_FETCH_SUCCESS:
return normalizeList(state, ['directory'], action.accounts, action.next);
return normalizeList(state, ['directory'], action.accounts);
case DIRECTORY_EXPAND_SUCCESS:
return appendToList(state, ['directory'], action.accounts, action.next);
return appendToList(state, ['directory'], action.accounts, null);
case DIRECTORY_FETCH_REQUEST:
case DIRECTORY_EXPAND_REQUEST:
return state.setIn(['directory', 'isLoading'], true);

View file

@ -1,118 +0,0 @@
import WebSocketClient from '@gamestdio/websocket';
import { getAccessToken } from 'soapbox/utils/auth';
import type { AppDispatch, RootState } from 'soapbox/store';
const randomIntUpTo = (max: number) => Math.floor(Math.random() * Math.floor(max));
interface ConnectStreamCallbacks {
onReceive(websocket: WebSocket, data: unknown): void;
}
type PollingRefreshFn = (dispatch: AppDispatch, done?: () => void) => void
const connectStream = (
path: string,
pollingRefresh: PollingRefreshFn | null = null,
callbacks: (dispatch: AppDispatch, getState: () => RootState) => ConnectStreamCallbacks,
) => (dispatch: AppDispatch, getState: () => RootState) => {
const streamingAPIBaseURL = getState().instance.configuration.urls.streaming;
const accessToken = getAccessToken(getState());
const { onReceive } = callbacks(dispatch, getState);
let polling: NodeJS.Timeout | null = null;
const setupPolling = () => {
if (pollingRefresh) {
pollingRefresh(dispatch, () => {
polling = setTimeout(() => setupPolling(), 20000 + randomIntUpTo(20000));
});
}
};
const clearPolling = () => {
if (polling) {
clearTimeout(polling);
polling = null;
}
};
let subscription: WebSocket;
// If the WebSocket fails to be created, don't crash the whole page,
// just proceed without a subscription.
try {
subscription = getStream(streamingAPIBaseURL!, accessToken!, path, {
connected() {
if (pollingRefresh) {
clearPolling();
}
},
disconnected() {
if (pollingRefresh) {
polling = setTimeout(() => setupPolling(), randomIntUpTo(40000));
}
},
received(data) {
onReceive(subscription, data);
},
reconnected() {
if (pollingRefresh) {
clearPolling();
pollingRefresh(dispatch);
}
},
});
} catch (e) {
console.error(e);
}
const disconnect = () => {
if (subscription) {
subscription.close();
}
clearPolling();
};
return disconnect;
};
const getStream = (
streamingAPIBaseURL: string,
accessToken: string,
stream: string,
{ connected, received, disconnected, reconnected }: {
connected: ((this: WebSocket, ev: Event) => any) | null;
received: (data: any) => void;
disconnected: ((this: WebSocket, ev: Event) => any) | null;
reconnected: ((this: WebSocket, ev: Event) => any);
},
) => {
const params = [ `access_token=${accessToken}`, `stream=${stream}` ];
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken as any);
ws.onopen = connected;
ws.onclose = disconnected;
ws.onreconnect = reconnected;
ws.onmessage = (e) => {
if (!e.data) return;
try {
received(JSON.parse(e.data));
} catch (error) {
console.error(e);
console.error(`Could not parse the above streaming event.\n${error}`);
}
};
return ws;
};
export { connectStream };

View file

@ -1617,11 +1617,6 @@
tslib "^2.4.0"
typescript "^4.7 || 5"
"@gamestdio/websocket@^0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@gamestdio/websocket/-/websocket-0.3.2.tgz#321ba0976ee30fd14e51dbf8faa85ce7b325f76a"
integrity sha512-J3n5SKim+ZoLbe44hRGI/VYAwSMCeIJuBy+FfP6EZaujEpNchPRFcIsVQLWAwpU1bP2Ji63rC+rEUOd1vjUB6Q==
"@gitbeaker/core@^35.8.0":
version "35.8.0"
resolved "https://registry.yarnpkg.com/@gitbeaker/core/-/core-35.8.0.tgz#8e55950dd6c45e6b48791432a1fa2c13b9460d39"