cleanup
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
1549043c0a
commit
6f1c11b39f
15 changed files with 36 additions and 183 deletions
|
@ -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",
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 },
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -67,8 +67,6 @@ const ModalRoot: React.FC = () => {
|
|||
}));
|
||||
|
||||
const onClickClose = (type?: ModalType) => {
|
||||
if (!type) return;
|
||||
|
||||
switch (type) {
|
||||
case 'COMPOSE':
|
||||
dispatch(cancelReplyCompose());
|
||||
|
|
|
@ -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']));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
118
src/stream.ts
118
src/stream.ts
|
@ -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 };
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue