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
					
				| 
						 | 
				
			
			@ -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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 New Issue