kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'main' into remove-immutable-compose
commit
1e65434824
|
@ -1,6 +1,7 @@
|
||||||
import { HTTPError } from 'soapbox/api/HTTPError.ts';
|
import { HTTPError } from 'soapbox/api/HTTPError.ts';
|
||||||
import { importEntities } from 'soapbox/entity-store/actions.ts';
|
import { importEntities } from 'soapbox/entity-store/actions.ts';
|
||||||
import { Entities } from 'soapbox/entity-store/entities.ts';
|
import { Entities } from 'soapbox/entity-store/entities.ts';
|
||||||
|
import { relationshipSchema } from 'soapbox/schemas/relationship.ts';
|
||||||
import { selectAccount } from 'soapbox/selectors/index.ts';
|
import { selectAccount } from 'soapbox/selectors/index.ts';
|
||||||
import { isLoggedIn } from 'soapbox/utils/auth.ts';
|
import { isLoggedIn } from 'soapbox/utils/auth.ts';
|
||||||
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features.ts';
|
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features.ts';
|
||||||
|
@ -15,7 +16,7 @@ import {
|
||||||
|
|
||||||
import type { Map as ImmutableMap } from 'immutable';
|
import type { Map as ImmutableMap } from 'immutable';
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store.ts';
|
import type { AppDispatch, RootState } from 'soapbox/store.ts';
|
||||||
import type { APIEntity, Status } from 'soapbox/types/entities.ts';
|
import type { APIEntity, Relationship, Status } from 'soapbox/types/entities.ts';
|
||||||
import type { History } from 'soapbox/types/history.ts';
|
import type { History } from 'soapbox/types/history.ts';
|
||||||
|
|
||||||
const ACCOUNT_CREATE_REQUEST = 'ACCOUNT_CREATE_REQUEST';
|
const ACCOUNT_CREATE_REQUEST = 'ACCOUNT_CREATE_REQUEST';
|
||||||
|
@ -609,7 +610,7 @@ const expandFollowingFail = (id: string, error: unknown) => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchRelationships = (accountIds: string[]) =>
|
const fetchRelationships = (accountIds: string[]) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
if (!isLoggedIn(getState)) return null;
|
if (!isLoggedIn(getState)) return null;
|
||||||
|
|
||||||
const loadedRelationships = getState().relationships;
|
const loadedRelationships = getState().relationships;
|
||||||
|
@ -621,15 +622,32 @@ const fetchRelationships = (accountIds: string[]) =>
|
||||||
|
|
||||||
dispatch(fetchRelationshipsRequest(newAccountIds));
|
dispatch(fetchRelationshipsRequest(newAccountIds));
|
||||||
|
|
||||||
return api(getState)
|
const results: Relationship[] = [];
|
||||||
.get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`)
|
|
||||||
.then((response) => response.json()).then((data) => {
|
try {
|
||||||
dispatch(importEntities(data, Entities.RELATIONSHIPS));
|
for (const ids of chunkArray(newAccountIds, 20)) {
|
||||||
dispatch(fetchRelationshipsSuccess(data));
|
const response = await api(getState).get('/api/v1/accounts/relationships', { searchParams: { id: ids } });
|
||||||
})
|
const json = await response.json();
|
||||||
.catch(error => dispatch(fetchRelationshipsFail(error)));
|
const data = relationshipSchema.array().parse(json);
|
||||||
|
|
||||||
|
results.push(...data);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(importEntities(results, Entities.RELATIONSHIPS));
|
||||||
|
dispatch(fetchRelationshipsSuccess(results));
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(fetchRelationshipsFail(error));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function* chunkArray<T>(array: T[], chunkSize: number): Iterable<T[]> {
|
||||||
|
if (chunkSize <= 0) throw new Error('Chunk size must be greater than zero.');
|
||||||
|
|
||||||
|
for (let i = 0; i < array.length; i += chunkSize) {
|
||||||
|
yield array.slice(i, i + chunkSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fetchRelationshipsRequest = (ids: string[]) => ({
|
const fetchRelationshipsRequest = (ids: string[]) => ({
|
||||||
type: RELATIONSHIPS_FETCH_REQUEST,
|
type: RELATIONSHIPS_FETCH_REQUEST,
|
||||||
ids,
|
ids,
|
||||||
|
|
|
@ -99,10 +99,9 @@ interface TimelineStreamOpts {
|
||||||
const connectTimelineStream = (
|
const connectTimelineStream = (
|
||||||
timelineId: string,
|
timelineId: string,
|
||||||
path: string,
|
path: string,
|
||||||
pollingRefresh: ((dispatch: AppDispatch, done?: () => void) => void) | null = null,
|
|
||||||
accept: ((status: APIEntity) => boolean) | null = null,
|
accept: ((status: APIEntity) => boolean) | null = null,
|
||||||
opts?: TimelineStreamOpts,
|
opts?: TimelineStreamOpts,
|
||||||
) => connectStream(path, pollingRefresh, (dispatch: AppDispatch, getState: () => RootState) => {
|
) => connectStream(path, (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const locale = getLocale(getState());
|
const locale = getLocale(getState());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { MastodonResponse } from 'soapbox/api/MastodonResponse.ts';
|
||||||
import { Entities } from 'soapbox/entity-store/entities.ts';
|
import { Entities } from 'soapbox/entity-store/entities.ts';
|
||||||
import { useBatchedEntities } from 'soapbox/entity-store/hooks/useBatchedEntities.ts';
|
import { useBatchedEntities } from 'soapbox/entity-store/hooks/useBatchedEntities.ts';
|
||||||
import { useApi } from 'soapbox/hooks/useApi.ts';
|
import { useApi } from 'soapbox/hooks/useApi.ts';
|
||||||
|
@ -8,9 +9,19 @@ function useRelationships(listKey: string[], ids: string[]) {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
const { isLoggedIn } = useLoggedIn();
|
const { isLoggedIn } = useLoggedIn();
|
||||||
|
|
||||||
function fetchRelationships(ids: string[]) {
|
async function fetchRelationships(ids: string[]) {
|
||||||
const q = ids.map((id) => `id[]=${id}`).join('&');
|
const results: Relationship[] = [];
|
||||||
return api.get(`/api/v1/accounts/relationships?${q}`);
|
|
||||||
|
for (const id of chunkArray(ids, 20)) {
|
||||||
|
const response = await api.get('/api/v1/accounts/relationships', { searchParams: { id } });
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
results.push(...json);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MastodonResponse(JSON.stringify(results), {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { entityMap: relationships, ...result } = useBatchedEntities<Relationship>(
|
const { entityMap: relationships, ...result } = useBatchedEntities<Relationship>(
|
||||||
|
@ -23,4 +34,12 @@ function useRelationships(listKey: string[], ids: string[]) {
|
||||||
return { relationships, ...result };
|
return { relationships, ...result };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function* chunkArray<T>(array: T[], chunkSize: number): Iterable<T[]> {
|
||||||
|
if (chunkSize <= 0) throw new Error('Chunk size must be greater than zero.');
|
||||||
|
|
||||||
|
for (let i = 0; i < array.length; i += chunkSize) {
|
||||||
|
yield array.slice(i, i + chunkSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export { useRelationships };
|
export { useRelationships };
|
|
@ -10,7 +10,6 @@ function useCommunityStream({ onlyMedia, enabled }: UseCommunityStreamOpts = {})
|
||||||
`community${onlyMedia ? ':media' : ''}`,
|
`community${onlyMedia ? ':media' : ''}`,
|
||||||
`public:local${onlyMedia ? ':media' : ''}`,
|
`public:local${onlyMedia ? ':media' : ''}`,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
|
||||||
{ enabled },
|
{ enabled },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ function useDirectStream() {
|
||||||
'direct',
|
'direct',
|
||||||
'direct',
|
'direct',
|
||||||
null,
|
null,
|
||||||
null,
|
|
||||||
{ enabled: isLoggedIn },
|
{ enabled: isLoggedIn },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ function useListStream(listId: string) {
|
||||||
`list:${listId}`,
|
`list:${listId}`,
|
||||||
`list&list=${listId}`,
|
`list&list=${listId}`,
|
||||||
null,
|
null,
|
||||||
null,
|
|
||||||
{ enabled: isLoggedIn },
|
{ enabled: isLoggedIn },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ function usePublicStream({ onlyMedia, language }: UsePublicStreamOpts = {}) {
|
||||||
`public${onlyMedia ? ':media' : ''}`,
|
`public${onlyMedia ? ':media' : ''}`,
|
||||||
`public${onlyMedia ? ':media' : ''}`,
|
`public${onlyMedia ? ':media' : ''}`,
|
||||||
null,
|
null,
|
||||||
null,
|
|
||||||
{ enabled: !language }, // TODO: support language streaming
|
{ enabled: !language }, // TODO: support language streaming
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { getAccessToken } from 'soapbox/utils/auth.ts';
|
||||||
function useTimelineStream(...args: Parameters<typeof connectTimelineStream>) {
|
function useTimelineStream(...args: Parameters<typeof connectTimelineStream>) {
|
||||||
// TODO: get rid of streaming.ts and move the actual opts here.
|
// TODO: get rid of streaming.ts and move the actual opts here.
|
||||||
const [timelineId, path] = args;
|
const [timelineId, path] = args;
|
||||||
const { enabled = true } = args[4] ?? {};
|
const { enabled = true } = args[3] ?? {};
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { instance } = useInstance();
|
const { instance } = useInstance();
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
import { expandNotifications } from 'soapbox/actions/notifications.ts';
|
|
||||||
import { expandHomeTimeline } from 'soapbox/actions/timelines.ts';
|
|
||||||
import { useStatContext } from 'soapbox/contexts/stat-context.tsx';
|
import { useStatContext } from 'soapbox/contexts/stat-context.tsx';
|
||||||
import { useLoggedIn } from 'soapbox/hooks/useLoggedIn.ts';
|
import { useLoggedIn } from 'soapbox/hooks/useLoggedIn.ts';
|
||||||
|
|
||||||
import { useTimelineStream } from './useTimelineStream.ts';
|
import { useTimelineStream } from './useTimelineStream.ts';
|
||||||
|
|
||||||
import type { AppDispatch } from 'soapbox/store.ts';
|
|
||||||
|
|
||||||
function useUserStream() {
|
function useUserStream() {
|
||||||
const { isLoggedIn } = useLoggedIn();
|
const { isLoggedIn } = useLoggedIn();
|
||||||
const statContext = useStatContext();
|
const statContext = useStatContext();
|
||||||
|
@ -14,16 +10,9 @@ function useUserStream() {
|
||||||
return useTimelineStream(
|
return useTimelineStream(
|
||||||
'home',
|
'home',
|
||||||
'user',
|
'user',
|
||||||
refresh,
|
|
||||||
null,
|
null,
|
||||||
{ statContext, enabled: isLoggedIn },
|
{ statContext, enabled: isLoggedIn },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Refresh home timeline and notifications. */
|
|
||||||
function refresh(dispatch: AppDispatch, done?: () => void) {
|
|
||||||
return dispatch(expandHomeTimeline({}, () =>
|
|
||||||
dispatch(expandNotifications({}, done))));
|
|
||||||
}
|
|
||||||
|
|
||||||
export { useUserStream };
|
export { useUserStream };
|
|
@ -293,7 +293,7 @@ const getSimplePolicy = createSelector([
|
||||||
});
|
});
|
||||||
|
|
||||||
const getRemoteInstanceFavicon = (state: RootState, host: string) => {
|
const getRemoteInstanceFavicon = (state: RootState, host: string) => {
|
||||||
const accounts = state.entities[Entities.ACCOUNTS]?.store as EntityStore<AccountSchema>;
|
const accounts = (state.entities[Entities.ACCOUNTS]?.store ?? {}) as EntityStore<AccountSchema>;
|
||||||
const account = Object.entries(accounts).find(([_, account]) => account && getDomain(account) === host)?.[1];
|
const account = Object.entries(accounts).find(([_, account]) => account && getDomain(account) === host)?.[1];
|
||||||
return account?.pleroma?.favicon;
|
return account?.pleroma?.favicon;
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,19 +4,14 @@ import { getAccessToken } from 'soapbox/utils/auth.ts';
|
||||||
|
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store.ts';
|
import type { AppDispatch, RootState } from 'soapbox/store.ts';
|
||||||
|
|
||||||
const randomIntUpTo = (max: number) => Math.floor(Math.random() * Math.floor(max));
|
|
||||||
|
|
||||||
interface ConnectStreamCallbacks {
|
interface ConnectStreamCallbacks {
|
||||||
onConnect(): void;
|
onConnect(): void;
|
||||||
onDisconnect(): void;
|
onDisconnect(): void;
|
||||||
onReceive(websocket: Websocket, data: unknown): void;
|
onReceive(websocket: Websocket, data: unknown): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PollingRefreshFn = (dispatch: AppDispatch, done?: () => void) => void
|
|
||||||
|
|
||||||
export function connectStream(
|
export function connectStream(
|
||||||
path: string,
|
path: string,
|
||||||
pollingRefresh: PollingRefreshFn | null = null,
|
|
||||||
callbacks: (dispatch: AppDispatch, getState: () => RootState) => ConnectStreamCallbacks,
|
callbacks: (dispatch: AppDispatch, getState: () => RootState) => ConnectStreamCallbacks,
|
||||||
) {
|
) {
|
||||||
return (dispatch: AppDispatch, getState: () => RootState) => {
|
return (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
@ -24,23 +19,6 @@ export function connectStream(
|
||||||
const accessToken = getAccessToken(getState());
|
const accessToken = getAccessToken(getState());
|
||||||
const { onConnect, onDisconnect, onReceive } = callbacks(dispatch, getState);
|
const { onConnect, onDisconnect, 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;
|
let subscription: Websocket;
|
||||||
|
|
||||||
// If the WebSocket fails to be created, don't crash the whole page,
|
// If the WebSocket fails to be created, don't crash the whole page,
|
||||||
|
@ -48,18 +26,10 @@ export function connectStream(
|
||||||
try {
|
try {
|
||||||
subscription = getStream(streamingAPIBaseURL!, accessToken!, path, {
|
subscription = getStream(streamingAPIBaseURL!, accessToken!, path, {
|
||||||
connected() {
|
connected() {
|
||||||
if (pollingRefresh) {
|
|
||||||
clearPolling();
|
|
||||||
}
|
|
||||||
|
|
||||||
onConnect();
|
onConnect();
|
||||||
},
|
},
|
||||||
|
|
||||||
disconnected() {
|
disconnected() {
|
||||||
if (pollingRefresh) {
|
|
||||||
polling = setTimeout(() => setupPolling(), randomIntUpTo(40000));
|
|
||||||
}
|
|
||||||
|
|
||||||
onDisconnect();
|
onDisconnect();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -68,11 +38,6 @@ export function connectStream(
|
||||||
},
|
},
|
||||||
|
|
||||||
reconnected() {
|
reconnected() {
|
||||||
if (pollingRefresh) {
|
|
||||||
clearPolling();
|
|
||||||
pollingRefresh(dispatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
onConnect();
|
onConnect();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -85,8 +50,6 @@ export function connectStream(
|
||||||
if (subscription) {
|
if (subscription) {
|
||||||
subscription.close();
|
subscription.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearPolling();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return disconnect;
|
return disconnect;
|
||||||
|
|
Ładowanie…
Reference in New Issue