mirror of
https://git.kescher.at/CatCatNya/catstodon.git
synced 2024-11-22 17:38:07 +01:00
Merge commit 'b6fd14f0e2842eca269ef8962e3c5bd560a76357' into glitch-soc/merge-upstream
Conflicts: - `app/lib/activitypub/parser/status_parser.rb`: Glitch-soc had changes to adjacent lines. Ported upstream's changes.
This commit is contained in:
commit
51631c785f
16 changed files with 346 additions and 127 deletions
|
@ -1,18 +1,10 @@
|
|||
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
|
||||
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
|
||||
import { apiSubmitAccountNote } from 'mastodon/api/accounts';
|
||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
export const submitAccountNote = createAppAsyncThunk(
|
||||
export const submitAccountNote = createDataLoadingThunk(
|
||||
'account_note/submit',
|
||||
async (args: { id: string; value: string }) => {
|
||||
const response = await api().post<ApiRelationshipJSON>(
|
||||
`/api/v1/accounts/${args.id}/note`,
|
||||
{
|
||||
comment: args.value,
|
||||
},
|
||||
);
|
||||
|
||||
return { relationship: response.data };
|
||||
},
|
||||
({ accountId, note }: { accountId: string; note: string }) =>
|
||||
apiSubmitAccountNote(accountId, note),
|
||||
(relationship) => ({ relationship }),
|
||||
{ skipLoading: true },
|
||||
);
|
||||
|
|
|
@ -3,10 +3,6 @@ import api, { getLinks } from '../api';
|
|||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||
|
||||
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
||||
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
||||
export const REBLOG_FAIL = 'REBLOG_FAIL';
|
||||
|
||||
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
|
||||
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
|
||||
export const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL';
|
||||
|
@ -15,10 +11,6 @@ export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
|
|||
export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
|
||||
export const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
|
||||
|
||||
export const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST';
|
||||
export const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS';
|
||||
export const UNREBLOG_FAIL = 'UNREBLOG_FAIL';
|
||||
|
||||
export const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST';
|
||||
export const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS';
|
||||
export const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL';
|
||||
|
@ -51,83 +43,7 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
|
|||
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
|
||||
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
|
||||
|
||||
export function reblog(status, visibility) {
|
||||
return function (dispatch) {
|
||||
dispatch(reblogRequest(status));
|
||||
|
||||
api().post(`/api/v1/statuses/${status.get('id')}/reblog`, { visibility }).then(function (response) {
|
||||
// The reblog API method returns a new status wrapped around the original. In this case we are only
|
||||
// interested in how the original is modified, hence passing it skipping the wrapper
|
||||
dispatch(importFetchedStatus(response.data.reblog));
|
||||
dispatch(reblogSuccess(status));
|
||||
}).catch(function (error) {
|
||||
dispatch(reblogFail(status, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function unreblog(status) {
|
||||
return (dispatch) => {
|
||||
dispatch(unreblogRequest(status));
|
||||
|
||||
api().post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(unreblogSuccess(status));
|
||||
}).catch(error => {
|
||||
dispatch(unreblogFail(status, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function reblogRequest(status) {
|
||||
return {
|
||||
type: REBLOG_REQUEST,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function reblogSuccess(status) {
|
||||
return {
|
||||
type: REBLOG_SUCCESS,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function reblogFail(status, error) {
|
||||
return {
|
||||
type: REBLOG_FAIL,
|
||||
status: status,
|
||||
error: error,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function unreblogRequest(status) {
|
||||
return {
|
||||
type: UNREBLOG_REQUEST,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function unreblogSuccess(status) {
|
||||
return {
|
||||
type: UNREBLOG_SUCCESS,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function unreblogFail(status, error) {
|
||||
return {
|
||||
type: UNREBLOG_FAIL,
|
||||
status: status,
|
||||
error: error,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
export * from "./interactions_typed";
|
||||
|
||||
export function favourite(status) {
|
||||
return function (dispatch) {
|
||||
|
|
35
app/javascript/mastodon/actions/interactions_typed.ts
Normal file
35
app/javascript/mastodon/actions/interactions_typed.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { apiReblog, apiUnreblog } from 'mastodon/api/interactions';
|
||||
import type { StatusVisibility } from 'mastodon/models/status';
|
||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
import { importFetchedStatus } from './importer';
|
||||
|
||||
export const reblog = createDataLoadingThunk(
|
||||
'status/reblog',
|
||||
({
|
||||
statusId,
|
||||
visibility,
|
||||
}: {
|
||||
statusId: string;
|
||||
visibility: StatusVisibility;
|
||||
}) => apiReblog(statusId, visibility),
|
||||
(data, { dispatch, discardLoadData }) => {
|
||||
// The reblog API method returns a new status wrapped around the original. In this case we are only
|
||||
// interested in how the original is modified, hence passing it skipping the wrapper
|
||||
dispatch(importFetchedStatus(data.reblog));
|
||||
|
||||
// The payload is not used in any actions
|
||||
return discardLoadData;
|
||||
},
|
||||
);
|
||||
|
||||
export const unreblog = createDataLoadingThunk(
|
||||
'status/unreblog',
|
||||
({ statusId }: { statusId: string }) => apiUnreblog(statusId),
|
||||
(data, { dispatch, discardLoadData }) => {
|
||||
dispatch(importFetchedStatus(data));
|
||||
|
||||
// The payload is not used in any actions
|
||||
return discardLoadData;
|
||||
},
|
||||
);
|
|
@ -1,4 +1,4 @@
|
|||
import type { AxiosResponse, RawAxiosRequestHeaders } from 'axios';
|
||||
import type { AxiosResponse, Method, RawAxiosRequestHeaders } from 'axios';
|
||||
import axios from 'axios';
|
||||
import LinkHeader from 'http-link-header';
|
||||
|
||||
|
@ -58,3 +58,17 @@ export default function api(withAuthorization = true) {
|
|||
],
|
||||
});
|
||||
}
|
||||
|
||||
export async function apiRequest<ApiResponse = unknown>(
|
||||
method: Method,
|
||||
url: string,
|
||||
params?: Record<string, unknown>,
|
||||
) {
|
||||
const { data } = await api().request<ApiResponse>({
|
||||
method,
|
||||
url: '/api/' + url,
|
||||
data: params,
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
|
7
app/javascript/mastodon/api/accounts.ts
Normal file
7
app/javascript/mastodon/api/accounts.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { apiRequest } from 'mastodon/api';
|
||||
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
|
||||
|
||||
export const apiSubmitAccountNote = (id: string, value: string) =>
|
||||
apiRequest<ApiRelationshipJSON>('post', `v1/accounts/${id}/note`, {
|
||||
comment: value,
|
||||
});
|
10
app/javascript/mastodon/api/interactions.ts
Normal file
10
app/javascript/mastodon/api/interactions.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { apiRequest } from 'mastodon/api';
|
||||
import type { Status, StatusVisibility } from 'mastodon/models/status';
|
||||
|
||||
export const apiReblog = (statusId: string, visibility: StatusVisibility) =>
|
||||
apiRequest<{ reblog: Status }>('post', `v1/statuses/${statusId}/reblog`, {
|
||||
visibility,
|
||||
});
|
||||
|
||||
export const apiUnreblog = (statusId: string) =>
|
||||
apiRequest<Status>('post', `v1/statuses/${statusId}/unreblog`);
|
|
@ -96,9 +96,9 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
|||
|
||||
onModalReblog (status, privacy) {
|
||||
if (status.get('reblogged')) {
|
||||
dispatch(unreblog(status));
|
||||
dispatch(unreblog({ statusId: status.get('id') }));
|
||||
} else {
|
||||
dispatch(reblog(status, privacy));
|
||||
dispatch(reblog({ statusId: status.get('id'), visibility: privacy }));
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ const mapStateToProps = (state, { account }) => ({
|
|||
const mapDispatchToProps = (dispatch, { account }) => ({
|
||||
|
||||
onSave (value) {
|
||||
dispatch(submitAccountNote({ id: account.get('id'), value}));
|
||||
dispatch(submitAccountNote({ accountId: account.get('id'), note: value }));
|
||||
},
|
||||
|
||||
});
|
||||
|
|
|
@ -39,12 +39,12 @@ const mapDispatchToProps = dispatch => ({
|
|||
},
|
||||
|
||||
onModalReblog (status, privacy) {
|
||||
dispatch(reblog(status, privacy));
|
||||
dispatch(reblog({ statusId: status.get('id'), visibility: privacy }));
|
||||
},
|
||||
|
||||
onReblog (status, e) {
|
||||
if (status.get('reblogged')) {
|
||||
dispatch(unreblog(status));
|
||||
dispatch(unreblog({ statusId: status.get('id') }));
|
||||
} else {
|
||||
if (e.shiftKey || !boostModal) {
|
||||
this.onModalReblog(status);
|
||||
|
|
|
@ -123,7 +123,7 @@ class Footer extends ImmutablePureComponent {
|
|||
|
||||
_performReblog = (status, privacy) => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(reblog(status, privacy));
|
||||
dispatch(reblog({ statusId: status.get('id'), visibility: privacy }));
|
||||
};
|
||||
|
||||
handleReblogClick = e => {
|
||||
|
@ -132,7 +132,7 @@ class Footer extends ImmutablePureComponent {
|
|||
|
||||
if (signedIn) {
|
||||
if (status.get('reblogged')) {
|
||||
dispatch(unreblog(status));
|
||||
dispatch(unreblog({ statusId: status.get('id') }));
|
||||
} else if ((e && e.shiftKey) || !boostModal) {
|
||||
this._performReblog(status);
|
||||
} else {
|
||||
|
|
|
@ -74,12 +74,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
},
|
||||
|
||||
onModalReblog (status, privacy) {
|
||||
dispatch(reblog(status, privacy));
|
||||
dispatch(reblog({ statusId: status.get('id'), visibility: privacy }));
|
||||
},
|
||||
|
||||
onReblog (status, e) {
|
||||
if (status.get('reblogged')) {
|
||||
dispatch(unreblog(status));
|
||||
dispatch(unreblog({ statusId: status.get('id') }));
|
||||
} else {
|
||||
if (e.shiftKey || !boostModal) {
|
||||
this.onModalReblog(status);
|
||||
|
|
|
@ -299,7 +299,7 @@ class Status extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleModalReblog = (status, privacy) => {
|
||||
this.props.dispatch(reblog(status, privacy));
|
||||
this.props.dispatch(reblog({ statusId: status.get('id'), visibility: privacy }));
|
||||
};
|
||||
|
||||
handleReblogClick = (status, e) => {
|
||||
|
@ -308,7 +308,7 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
if (signedIn) {
|
||||
if (status.get('reblogged')) {
|
||||
dispatch(unreblog(status));
|
||||
dispatch(unreblog({ statusId: status.get('id') }));
|
||||
} else {
|
||||
if ((e && e.shiftKey) || !boostModal) {
|
||||
this.handleModalReblog(status);
|
||||
|
|
|
@ -3,10 +3,6 @@ import { Map as ImmutableMap, fromJS } from 'immutable';
|
|||
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
|
||||
import { normalizeStatusTranslation } from '../actions/importer/normalizer';
|
||||
import {
|
||||
REBLOG_REQUEST,
|
||||
REBLOG_FAIL,
|
||||
UNREBLOG_REQUEST,
|
||||
UNREBLOG_FAIL,
|
||||
FAVOURITE_REQUEST,
|
||||
FAVOURITE_FAIL,
|
||||
UNFAVOURITE_REQUEST,
|
||||
|
@ -16,6 +12,10 @@ import {
|
|||
UNBOOKMARK_REQUEST,
|
||||
UNBOOKMARK_FAIL,
|
||||
} from '../actions/interactions';
|
||||
import {
|
||||
reblog,
|
||||
unreblog,
|
||||
} from '../actions/interactions_typed';
|
||||
import {
|
||||
STATUS_MUTE_SUCCESS,
|
||||
STATUS_UNMUTE_SUCCESS,
|
||||
|
@ -65,6 +65,7 @@ const statusTranslateUndo = (state, id) => {
|
|||
|
||||
const initialState = ImmutableMap();
|
||||
|
||||
/** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */
|
||||
export default function statuses(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case STATUS_FETCH_REQUEST:
|
||||
|
@ -91,14 +92,6 @@ export default function statuses(state = initialState, action) {
|
|||
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'bookmarked'], false);
|
||||
case UNBOOKMARK_FAIL:
|
||||
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'bookmarked'], true);
|
||||
case REBLOG_REQUEST:
|
||||
return state.setIn([action.status.get('id'), 'reblogged'], true);
|
||||
case REBLOG_FAIL:
|
||||
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'reblogged'], false);
|
||||
case UNREBLOG_REQUEST:
|
||||
return state.setIn([action.status.get('id'), 'reblogged'], false);
|
||||
case UNREBLOG_FAIL:
|
||||
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'reblogged'], true);
|
||||
case STATUS_MUTE_SUCCESS:
|
||||
return state.setIn([action.id, 'muted'], true);
|
||||
case STATUS_UNMUTE_SUCCESS:
|
||||
|
@ -128,6 +121,15 @@ export default function statuses(state = initialState, action) {
|
|||
case STATUS_TRANSLATE_UNDO:
|
||||
return statusTranslateUndo(state, action.id);
|
||||
default:
|
||||
return state;
|
||||
if(reblog.pending.match(action))
|
||||
return state.setIn([action.meta.arg.statusId, 'reblogged'], true);
|
||||
else if(reblog.rejected.match(action))
|
||||
return state.get(action.meta.arg.statusId) === undefined ? state : state.setIn([action.meta.arg.statusId, 'reblogged'], false);
|
||||
else if(unreblog.pending.match(action))
|
||||
return state.setIn([action.meta.arg.statusId, 'reblogged'], false);
|
||||
else if(unreblog.rejected.match(action))
|
||||
return state.get(action.meta.arg.statusId) === undefined ? state : state.setIn([action.meta.arg.statusId, 'reblogged'], true);
|
||||
else
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
|
|||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import type { BaseThunkAPI } from '@reduxjs/toolkit/dist/createAsyncThunk';
|
||||
|
||||
import type { AppDispatch, RootState } from './store';
|
||||
|
||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
||||
|
@ -13,8 +15,192 @@ export interface AsyncThunkRejectValue {
|
|||
error?: unknown;
|
||||
}
|
||||
|
||||
interface AppMeta {
|
||||
skipLoading?: boolean;
|
||||
}
|
||||
|
||||
export const createAppAsyncThunk = createAsyncThunk.withTypes<{
|
||||
state: RootState;
|
||||
dispatch: AppDispatch;
|
||||
rejectValue: AsyncThunkRejectValue;
|
||||
}>();
|
||||
|
||||
type AppThunkApi = Pick<
|
||||
BaseThunkAPI<
|
||||
RootState,
|
||||
unknown,
|
||||
AppDispatch,
|
||||
AsyncThunkRejectValue,
|
||||
AppMeta,
|
||||
AppMeta
|
||||
>,
|
||||
'getState' | 'dispatch'
|
||||
>;
|
||||
|
||||
interface AppThunkOptions {
|
||||
skipLoading?: boolean;
|
||||
}
|
||||
|
||||
const createBaseAsyncThunk = createAsyncThunk.withTypes<{
|
||||
state: RootState;
|
||||
dispatch: AppDispatch;
|
||||
rejectValue: AsyncThunkRejectValue;
|
||||
fulfilledMeta: AppMeta;
|
||||
rejectedMeta: AppMeta;
|
||||
}>();
|
||||
|
||||
export function createThunk<Arg = void, Returned = void>(
|
||||
name: string,
|
||||
creator: (arg: Arg, api: AppThunkApi) => Returned | Promise<Returned>,
|
||||
options: AppThunkOptions = {},
|
||||
) {
|
||||
return createBaseAsyncThunk(
|
||||
name,
|
||||
async (
|
||||
arg: Arg,
|
||||
{ getState, dispatch, fulfillWithValue, rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const result = await creator(arg, { dispatch, getState });
|
||||
|
||||
return fulfillWithValue(result, {
|
||||
skipLoading: options.skipLoading,
|
||||
});
|
||||
} catch (error) {
|
||||
return rejectWithValue({ error }, { skipLoading: true });
|
||||
}
|
||||
},
|
||||
{
|
||||
getPendingMeta() {
|
||||
if (options.skipLoading) return { skipLoading: true };
|
||||
return {};
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const discardLoadDataInPayload = Symbol('discardLoadDataInPayload');
|
||||
type DiscardLoadData = typeof discardLoadDataInPayload;
|
||||
|
||||
type OnData<LoadDataResult, ReturnedData> = (
|
||||
data: LoadDataResult,
|
||||
api: AppThunkApi & {
|
||||
discardLoadData: DiscardLoadData;
|
||||
},
|
||||
) => ReturnedData | DiscardLoadData | Promise<ReturnedData | DiscardLoadData>;
|
||||
|
||||
// Overload when there is no `onData` method, the payload is the `onData` result
|
||||
export function createDataLoadingThunk<
|
||||
LoadDataResult,
|
||||
Args extends Record<string, unknown>,
|
||||
>(
|
||||
name: string,
|
||||
loadData: (args: Args) => Promise<LoadDataResult>,
|
||||
thunkOptions?: AppThunkOptions,
|
||||
): ReturnType<typeof createThunk<Args, LoadDataResult>>;
|
||||
|
||||
// Overload when the `onData` method returns discardLoadDataInPayload, then the payload is empty
|
||||
export function createDataLoadingThunk<
|
||||
LoadDataResult,
|
||||
Args extends Record<string, unknown>,
|
||||
>(
|
||||
name: string,
|
||||
loadData: (args: Args) => Promise<LoadDataResult>,
|
||||
onDataOrThunkOptions?:
|
||||
| AppThunkOptions
|
||||
| OnData<LoadDataResult, DiscardLoadData>,
|
||||
thunkOptions?: AppThunkOptions,
|
||||
): ReturnType<typeof createThunk<Args, void>>;
|
||||
|
||||
// Overload when the `onData` method returns nothing, then the mayload is the `onData` result
|
||||
export function createDataLoadingThunk<
|
||||
LoadDataResult,
|
||||
Args extends Record<string, unknown>,
|
||||
>(
|
||||
name: string,
|
||||
loadData: (args: Args) => Promise<LoadDataResult>,
|
||||
onDataOrThunkOptions?: AppThunkOptions | OnData<LoadDataResult, void>,
|
||||
thunkOptions?: AppThunkOptions,
|
||||
): ReturnType<typeof createThunk<Args, LoadDataResult>>;
|
||||
|
||||
// Overload when there is an `onData` method returning something
|
||||
export function createDataLoadingThunk<
|
||||
LoadDataResult,
|
||||
Args extends Record<string, unknown>,
|
||||
Returned,
|
||||
>(
|
||||
name: string,
|
||||
loadData: (args: Args) => Promise<LoadDataResult>,
|
||||
onDataOrThunkOptions?: AppThunkOptions | OnData<LoadDataResult, Returned>,
|
||||
thunkOptions?: AppThunkOptions,
|
||||
): ReturnType<typeof createThunk<Args, Returned>>;
|
||||
|
||||
/**
|
||||
* This function creates a Redux Thunk that handles loading data asynchronously (usually from the API), dispatching `pending`, `fullfilled` and `rejected` actions.
|
||||
*
|
||||
* You can run a callback on the `onData` results to either dispatch side effects or modify the payload.
|
||||
*
|
||||
* It is a wrapper around RTK's [`createAsyncThunk`](https://redux-toolkit.js.org/api/createAsyncThunk)
|
||||
* @param name Prefix for the actions types
|
||||
* @param loadData Function that loads the data. It's (object) argument will become the thunk's argument
|
||||
* @param onDataOrThunkOptions
|
||||
* Callback called on the results from `loadData`.
|
||||
*
|
||||
* First argument will be the return from `loadData`.
|
||||
*
|
||||
* Second argument is an object with: `dispatch`, `getState` and `discardLoadData`.
|
||||
* It can return:
|
||||
* - `undefined` (or no explicit return), meaning that the `onData` results will be the payload
|
||||
* - `discardLoadData` to discard the `onData` results and return an empty payload
|
||||
* - anything else, which will be the payload
|
||||
*
|
||||
* You can also omit this parameter and pass `thunkOptions` directly
|
||||
* @param maybeThunkOptions
|
||||
* Additional Mastodon specific options for the thunk. Currently supports:
|
||||
* - `skipLoading` to avoid showing the loading bar when the request is in progress
|
||||
* @returns The created thunk
|
||||
*/
|
||||
export function createDataLoadingThunk<
|
||||
LoadDataResult,
|
||||
Args extends Record<string, unknown>,
|
||||
Returned,
|
||||
>(
|
||||
name: string,
|
||||
loadData: (args: Args) => Promise<LoadDataResult>,
|
||||
onDataOrThunkOptions?: AppThunkOptions | OnData<LoadDataResult, Returned>,
|
||||
maybeThunkOptions?: AppThunkOptions,
|
||||
) {
|
||||
let onData: OnData<LoadDataResult, Returned> | undefined;
|
||||
let thunkOptions: AppThunkOptions | undefined;
|
||||
|
||||
if (typeof onDataOrThunkOptions === 'function') onData = onDataOrThunkOptions;
|
||||
else if (typeof onDataOrThunkOptions === 'object')
|
||||
thunkOptions = onDataOrThunkOptions;
|
||||
|
||||
if (maybeThunkOptions) {
|
||||
thunkOptions = maybeThunkOptions;
|
||||
}
|
||||
|
||||
return createThunk<Args, Returned>(
|
||||
name,
|
||||
async (arg, { getState, dispatch }) => {
|
||||
const data = await loadData(arg);
|
||||
|
||||
if (!onData) return data as Returned;
|
||||
|
||||
const result = await onData(data, {
|
||||
dispatch,
|
||||
getState,
|
||||
discardLoadData: discardLoadDataInPayload,
|
||||
});
|
||||
|
||||
// if there is no return in `onData`, we return the `onData` result
|
||||
if (typeof result === 'undefined') return data as Returned;
|
||||
// the user explicitely asked to discard the payload
|
||||
else if (result === discardLoadDataInPayload)
|
||||
return undefined as Returned;
|
||||
else return result;
|
||||
},
|
||||
thunkOptions,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
class ActivityPub::Parser::StatusParser
|
||||
include JsonLdHelper
|
||||
|
||||
NORMALIZED_LOCALE_NAMES = LanguagesHelper::SUPPORTED_LOCALES.keys.index_by(&:downcase).freeze
|
||||
|
||||
# @param [Hash] json
|
||||
# @param [Hash] options
|
||||
# @option options [String] :followers_collection
|
||||
|
@ -89,6 +91,13 @@ class ActivityPub::Parser::StatusParser
|
|||
end
|
||||
|
||||
def language
|
||||
lang = raw_language_code
|
||||
lang.presence && NORMALIZED_LOCALE_NAMES.fetch(lang.downcase.to_sym, lang)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def raw_language_code
|
||||
if content_language_map?
|
||||
@object['contentMap'].keys.first
|
||||
elsif name_language_map?
|
||||
|
@ -102,8 +111,6 @@ class ActivityPub::Parser::StatusParser
|
|||
@object['directMessage']
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def audience_to
|
||||
as_array(@object['to'] || @json['to']).map { |x| value_or_id(x) }
|
||||
end
|
||||
|
|
50
spec/lib/activitypub/parser/status_parser_spec.rb
Normal file
50
spec/lib/activitypub/parser/status_parser_spec.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Parser::StatusParser do
|
||||
subject { described_class.new(json) }
|
||||
|
||||
let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor') }
|
||||
let(:follower) { Fabricate(:account, username: 'bob') }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#foo'].join,
|
||||
type: 'Create',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: object_json,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), 'post1'].join('/'),
|
||||
type: 'Note',
|
||||
to: [
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
ActivityPub::TagManager.instance.uri_for(follower),
|
||||
],
|
||||
content: '@bob lorem ipsum',
|
||||
contentMap: {
|
||||
EN: '@bob lorem ipsum',
|
||||
},
|
||||
published: 1.hour.ago.utc.iso8601,
|
||||
updated: 1.hour.ago.utc.iso8601,
|
||||
tag: {
|
||||
type: 'Mention',
|
||||
href: ActivityPub::TagManager.instance.uri_for(follower),
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
it 'correctly parses status' do
|
||||
expect(subject).to have_attributes(
|
||||
text: '@bob lorem ipsum',
|
||||
uri: [ActivityPub::TagManager.instance.uri_for(sender), 'post1'].join('/'),
|
||||
reply: false,
|
||||
language: :en
|
||||
)
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue