Merge branch 'refs/heads/glitch-soc' into develop

# Conflicts:
#	app/javascript/flavours/glitch/actions/interactions.js
#	app/javascript/flavours/glitch/reducers/statuses.js
This commit is contained in:
Jeremy Kescher 2024-05-29 14:23:08 +02:00
commit 6d448d46ff
No known key found for this signature in database
GPG key ID: 80A419A7A613DFA4
138 changed files with 1578 additions and 1087 deletions

View file

@ -103,6 +103,8 @@ gem 'rdf-normalize', '~> 0.5'
gem 'private_address_check', '~> 0.5'
gem 'opentelemetry-api', '~> 1.2.5'
group :opentelemetry do
gem 'opentelemetry-exporter-otlp', '~> 0.26.3', require: false
gem 'opentelemetry-instrumentation-active_job', '~> 0.7.1', require: false

View file

@ -732,7 +732,7 @@ GEM
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 8)
rspec-support (3.13.1)
rubocop (1.63.5)
rubocop (1.64.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
@ -883,7 +883,7 @@ GEM
webfinger (1.2.0)
activesupport
httpclient (>= 2.4)
webmock (3.23.0)
webmock (3.23.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@ -981,6 +981,7 @@ DEPENDENCIES
omniauth-rails_csrf_protection (~> 1.0)
omniauth-saml (~> 2.0)
omniauth_openid_connect (~> 0.6.1)
opentelemetry-api (~> 1.2.5)
opentelemetry-exporter-otlp (~> 0.26.3)
opentelemetry-instrumentation-active_job (~> 0.7.1)
opentelemetry-instrumentation-active_model_serializers (~> 0.20.1)

View file

@ -44,7 +44,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end
def build_resource(hash = nil)
super(hash)
super
resource.locale = I18n.locale
resource.invite_code = @invite&.code if resource.invite_code.blank?

View file

@ -1,18 +1,10 @@
import type { ApiRelationshipJSON } from 'flavours/glitch/api_types/relationships';
import { createAppAsyncThunk } from 'flavours/glitch/store/typed_functions';
import { apiSubmitAccountNote } from 'flavours/glitch/api/accounts';
import { createDataLoadingThunk } from 'flavours/glitch/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 },
);

View file

@ -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';
@ -61,83 +53,7 @@ export const REACTION_REMOVE_REQUEST = 'REACTION_REMOVE_REQUEST';
export const REACTION_REMOVE_SUCCESS = 'REACTION_REMOVE_SUCCESS';
export const REACTION_REMOVE_FAIL = 'REACTION_REMOVE_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) {

View file

@ -0,0 +1,35 @@
import { apiReblog, apiUnreblog } from 'flavours/glitch/api/interactions';
import type { StatusVisibility } from 'flavours/glitch/models/status';
import { createDataLoadingThunk } from 'flavours/glitch/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;
},
);

View file

@ -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';
@ -40,11 +40,11 @@ const authorizationTokenFromInitialState = (): RawAxiosRequestHeaders => {
};
// eslint-disable-next-line import/no-default-export
export default function api() {
export default function api(withAuthorization = true) {
return axios.create({
headers: {
...csrfHeader,
...authorizationTokenFromInitialState(),
...(withAuthorization ? authorizationTokenFromInitialState() : {}),
},
transformResponse: [
@ -58,3 +58,17 @@ export default function api() {
],
});
}
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;
}

View file

@ -0,0 +1,7 @@
import { apiRequest } from 'flavours/glitch/api';
import type { ApiRelationshipJSON } from 'flavours/glitch/api_types/relationships';
export const apiSubmitAccountNote = (id: string, value: string) =>
apiRequest<ApiRelationshipJSON>('post', `v1/accounts/${id}/note`, {
comment: value,
});

View file

@ -0,0 +1,10 @@
import { apiRequest } from 'flavours/glitch/api';
import type { Status, StatusVisibility } from 'flavours/glitch/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`);

View file

@ -48,7 +48,7 @@ export default class Counter extends PureComponent {
componentDidMount () {
const { measure, start_at, end_at, params } = this.props;
api().post('/api/v1/admin/measures', { keys: [measure], start_at, end_at, [measure]: params }).then(res => {
api(false).post('/api/v1/admin/measures', { keys: [measure], start_at, end_at, [measure]: params }).then(res => {
this.setState({
loading: false,
data: res.data,

View file

@ -26,7 +26,7 @@ export default class Dimension extends PureComponent {
componentDidMount () {
const { start_at, end_at, dimension, limit, params } = this.props;
api().post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit, [dimension]: params }).then(res => {
api(false).post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit, [dimension]: params }).then(res => {
this.setState({
loading: false,
data: res.data,

View file

@ -27,7 +27,7 @@ export default class ImpactReport extends PureComponent {
include_subdomains: true,
};
api().post('/api/v1/admin/measures', {
api(false).post('/api/v1/admin/measures', {
keys: ['instance_accounts', 'instance_follows', 'instance_followers'],
start_at: null,
end_at: null,

View file

@ -105,7 +105,7 @@ class ReportReasonSelector extends PureComponent {
};
componentDidMount() {
api().get('/api/v1/instance').then(res => {
api(false).get('/api/v1/instance').then(res => {
this.setState({
rules: res.data.rules,
});
@ -122,7 +122,7 @@ class ReportReasonSelector extends PureComponent {
return;
}
api().put(`/api/v1/admin/reports/${id}`, {
api(false).put(`/api/v1/admin/reports/${id}`, {
category,
rule_ids: category === 'violation' ? rule_ids : [],
}).catch(err => {

View file

@ -34,7 +34,7 @@ export default class Retention extends PureComponent {
componentDidMount () {
const { start_at, end_at, frequency } = this.props;
api().post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => {
api(false).post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => {
this.setState({
loading: false,
data: res.data,

View file

@ -22,7 +22,7 @@ export default class Trends extends PureComponent {
componentDidMount () {
const { limit } = this.props;
api().get('/api/v1/admin/trends/tags', { params: { limit } }).then(res => {
api(false).get('/api/v1/admin/trends/tags', { params: { limit } }).then(res => {
this.setState({
loading: false,
data: res.data,

View file

@ -117,9 +117,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 }));
}
},

View file

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

View file

@ -15,7 +15,7 @@ const mapStateToProps = (state, { columnId }) => {
return {
settings: columns.get(index).get('params'),
onLoad (value) {
return api(() => state).get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => {
return api().get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => {
return (response.data.hashtags || []).map((tag) => {
return { value: tag.name, label: `#${tag.name}` };
});

View file

@ -36,12 +36,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);

View file

@ -125,7 +125,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 => {
@ -134,7 +134,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 {

View file

@ -71,12 +71,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);

View file

@ -361,9 +361,9 @@ class Status extends ImmutablePureComponent {
const { dispatch } = this.props;
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 }));
}
};

View file

@ -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,
@ -21,6 +17,10 @@ import {
REACTION_ADD_REQUEST,
REACTION_REMOVE_REQUEST,
} from '../actions/interactions';
import {
reblog,
unreblog,
} from '../actions/interactions_typed';
import {
STATUS_MUTE_SUCCESS,
STATUS_UNMUTE_SUCCESS,
@ -107,6 +107,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:
@ -133,10 +134,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 REACTION_UPDATE:
return updateReactionCount(state, action.reaction);
case REACTION_ADD_REQUEST:
@ -145,10 +142,6 @@ export default function statuses(state = initialState, action) {
case REACTION_REMOVE_REQUEST:
case REACTION_ADD_FAIL:
return removeReaction(state, action.id, action.name);
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:
@ -178,6 +171,15 @@ export default function statuses(state = initialState, action) {
case STATUS_TRANSLATE_UNDO:
return statusTranslateUndo(state, action.id);
default:
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;
}
}

View file

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

View file

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

View file

@ -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) {

View 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;
},
);

View file

@ -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';
@ -40,11 +40,11 @@ const authorizationTokenFromInitialState = (): RawAxiosRequestHeaders => {
};
// eslint-disable-next-line import/no-default-export
export default function api() {
export default function api(withAuthorization = true) {
return axios.create({
headers: {
...csrfHeader,
...authorizationTokenFromInitialState(),
...(withAuthorization ? authorizationTokenFromInitialState() : {}),
},
transformResponse: [
@ -58,3 +58,17 @@ export default function api() {
],
});
}
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;
}

View 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,
});

View 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`);

View file

@ -48,7 +48,7 @@ export default class Counter extends PureComponent {
componentDidMount () {
const { measure, start_at, end_at, params } = this.props;
api().post('/api/v1/admin/measures', { keys: [measure], start_at, end_at, [measure]: params }).then(res => {
api(false).post('/api/v1/admin/measures', { keys: [measure], start_at, end_at, [measure]: params }).then(res => {
this.setState({
loading: false,
data: res.data,

View file

@ -26,7 +26,7 @@ export default class Dimension extends PureComponent {
componentDidMount () {
const { start_at, end_at, dimension, limit, params } = this.props;
api().post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit, [dimension]: params }).then(res => {
api(false).post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit, [dimension]: params }).then(res => {
this.setState({
loading: false,
data: res.data,

View file

@ -27,7 +27,7 @@ export default class ImpactReport extends PureComponent {
include_subdomains: true,
};
api().post('/api/v1/admin/measures', {
api(false).post('/api/v1/admin/measures', {
keys: ['instance_accounts', 'instance_follows', 'instance_followers'],
start_at: null,
end_at: null,

View file

@ -105,7 +105,7 @@ class ReportReasonSelector extends PureComponent {
};
componentDidMount() {
api().get('/api/v1/instance').then(res => {
api(false).get('/api/v1/instance').then(res => {
this.setState({
rules: res.data.rules,
});
@ -122,7 +122,7 @@ class ReportReasonSelector extends PureComponent {
return;
}
api().put(`/api/v1/admin/reports/${id}`, {
api(false).put(`/api/v1/admin/reports/${id}`, {
category,
rule_ids: category === 'violation' ? rule_ids : [],
}).catch(err => {

View file

@ -34,7 +34,7 @@ export default class Retention extends PureComponent {
componentDidMount () {
const { start_at, end_at, frequency } = this.props;
api().post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => {
api(false).post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => {
this.setState({
loading: false,
data: res.data,

View file

@ -22,7 +22,7 @@ export default class Trends extends PureComponent {
componentDidMount () {
const { limit } = this.props;
api().get('/api/v1/admin/trends/tags', { params: { limit } }).then(res => {
api(false).get('/api/v1/admin/trends/tags', { params: { limit } }).then(res => {
this.setState({
loading: false,
data: res.data,

View file

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

View file

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

View file

@ -15,7 +15,7 @@ const mapStateToProps = (state, { columnId }) => {
return {
settings: columns.get(index).get('params'),
onLoad (value) {
return api(() => state).get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => {
return api().get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => {
return (response.data.hashtags || []).map((tag) => {
return { value: tag.name, label: `#${tag.name}` };
});

View file

@ -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);

View file

@ -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 {

View file

@ -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);

View file

@ -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);

View file

@ -469,6 +469,7 @@
"notification.follow": "{name} падпісаўся на вас",
"notification.follow_request": "{name} адправіў запыт на падпіску",
"notification.mention": "{name} згадаў вас",
"notification.moderation-warning.learn_more": "Даведацца больш",
"notification.own_poll": "Ваша апытанне скончылася",
"notification.poll": "Апытанне, дзе вы прынялі ўдзел, скончылася",
"notification.reblog": "{name} пашырыў ваш допіс",

View file

@ -234,7 +234,7 @@
"embed.preview": "이렇게 표시됩니다:",
"emoji_button.activity": "활동",
"emoji_button.clear": "지우기",
"emoji_button.custom": "사용자 지정",
"emoji_button.custom": "커스텀",
"emoji_button.flags": "깃발",
"emoji_button.food": "음식과 마실것",
"emoji_button.label": "에모지 추가",

View file

@ -491,7 +491,7 @@
"onboarding.actions.go_to_home": "Dodieties uz manu mājas plūsmu",
"onboarding.compose.template": "Sveiki, #Mastodon!",
"onboarding.follows.empty": "Diemžēl pašlaik nevar parādīt rezultātus. Vari mēģināt izmantot meklēšanu vai pārlūkot izpētes lapu, lai atrastu cilvēkus, kuriem sekot, vai vēlāk mēģināt vēlreiz.",
"onboarding.follows.lead": "Tava mājas plūsma ir galvenais veids, kā izbaudīt Mastodon. Jo vairāk cilvēku sekosi, jo aktīvāk un interesantāk tas būs. Lai sāktu, šeit ir daži ieteikumi:",
"onboarding.follows.lead": "Tava mājas plūsma ir galvenais veids, kā pieredzēt Mastodon. Jo vairāk cilvēkiem sekosi, jo dzīvīgāka un aizraujošāka tā būs. Lai sāktu, šeit ir daži ieteikumi:",
"onboarding.follows.title": "Pielāgo savu mājas barotni",
"onboarding.profile.discoverable": "Padarīt manu profilu atklājamu",
"onboarding.profile.display_name": "Attēlojamais vārds",

View file

@ -297,6 +297,7 @@
"filter_modal.select_filter.subtitle": "Bruk ein eksisterande kategori eller opprett ein ny",
"filter_modal.select_filter.title": "Filtrer dette innlegget",
"filter_modal.title.status": "Filtrer eit innlegg",
"filtered_notifications_banner.mentions": "{count, plural, one {omtale} other {omtaler}}",
"filtered_notifications_banner.pending_requests": "Varsel frå {count, plural, =0 {ingen} one {ein person} other {# folk}} du kanskje kjenner",
"filtered_notifications_banner.title": "Filtrerte varslingar",
"firehose.all": "Alle",
@ -307,6 +308,8 @@
"follow_requests.unlocked_explanation": "Sjølv om kontoen din ikkje er låst tenkte dei som driv {domain} at du kanskje ville gå gjennom førespurnadar frå desse kontoane manuelt.",
"follow_suggestions.curated_suggestion": "Utvalt av staben",
"follow_suggestions.dismiss": "Ikkje vis igjen",
"follow_suggestions.featured_longer": "Hanplukka av gjengen på {domain}",
"follow_suggestions.friends_of_friends_longer": "Populært hjå dei du fylgjer",
"follow_suggestions.hints.featured": "Denne profilen er handplukka av folka på {domain}.",
"follow_suggestions.hints.friends_of_friends": "Denne profilen er populær hjå dei du fylgjer.",
"follow_suggestions.hints.most_followed": "Mange på {domain} fylgjer denne profilen.",
@ -314,6 +317,8 @@
"follow_suggestions.hints.similar_to_recently_followed": "Denne profilen liknar på dei andre profilane du har fylgt i det siste.",
"follow_suggestions.personalized_suggestion": "Personleg forslag",
"follow_suggestions.popular_suggestion": "Populært forslag",
"follow_suggestions.popular_suggestion_longer": "Populært på {domain}",
"follow_suggestions.similar_to_recently_followed_longer": "Liknar på profilar du har fylgt i det siste",
"follow_suggestions.view_all": "Vis alle",
"follow_suggestions.who_to_follow": "Kven du kan fylgja",
"followed_tags": "Fylgde emneknaggar",

View file

@ -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:
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;
}
}

View file

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

View file

@ -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

View file

@ -33,6 +33,6 @@ class ActivityPub::Serializer < ActiveModel::Serializer
adapter_options[:named_contexts].merge!(_named_contexts)
adapter_options[:context_extensions].merge!(_context_extensions)
end
super(adapter_options, options, adapter_instance)
super
end
end

View file

@ -31,7 +31,7 @@ class AdvancedTextFormatter < TextFormatter
# @option options [String] :content_type
def initialize(text, options = {})
@content_type = options.delete(:content_type)
super(text, options)
super
@text = format_markdown(text) if content_type == 'text/markdown'
end

View file

@ -5,7 +5,7 @@ require_relative 'shared_timed_stack'
class ConnectionPool::SharedConnectionPool < ConnectionPool
def initialize(options = {}, &block)
super(options, &block)
super
@available = ConnectionPool::SharedTimedStack.new(@size, &block)
end

View file

@ -2,7 +2,7 @@
class RSS::Channel < RSS::Element
def initialize
super()
super
@root = create_element('channel')
end

View file

@ -2,7 +2,7 @@
class RSS::Item < RSS::Element
def initialize
super()
super
@root = create_element('item')
end

View file

@ -23,7 +23,7 @@ module Attachmentable
included do
def self.has_attached_file(name, options = {}) # rubocop:disable Naming/PredicateName
super(name, options)
super
send(:"before_#{name}_validate", prepend: true) do
attachment = send(name)

View file

@ -151,13 +151,23 @@ class AccountSearchService < BaseService
end
def call(query, account = nil, options = {})
MastodonOTELTracer.in_span('AccountSearchService#call') do |span|
@query = query&.strip&.gsub(/\A@/, '')
@limit = options[:limit].to_i
@offset = options[:offset].to_i
@options = options
@account = account
search_service_results.compact.uniq
span.add_attributes(
'search.offset' => @offset,
'search.limit' => @limit,
'search.backend' => Chewy.enabled? ? 'elasticsearch' : 'database'
)
search_service_results.compact.uniq.tap do |results|
span.set_attribute('search.results.count', results.size)
end
end
end
private

View file

@ -2,14 +2,24 @@
class StatusesSearchService < BaseService
def call(query, account = nil, options = {})
MastodonOTELTracer.in_span('StatusesSearchService#call') do |span|
@query = query&.strip
@account = account
@options = options
@limit = options[:limit].to_i
@offset = options[:offset].to_i
convert_deprecated_options!
status_search_results
span.add_attributes(
'search.offset' => @offset,
'search.limit' => @limit,
'search.backend' => Chewy.enabled? ? 'elasticsearch' : 'database'
)
status_search_results.tap do |results|
span.set_attribute('search.results.count', results.size)
end
end
end
private

View file

@ -2,16 +2,26 @@
class TagSearchService < BaseService
def call(query, options = {})
MastodonOTELTracer.in_span('TagSearchService#call') do |span|
@query = query.strip.delete_prefix('#')
@offset = options.delete(:offset).to_i
@limit = options.delete(:limit).to_i
@options = options
span.add_attributes(
'search.offset' => @offset,
'search.limit' => @limit,
'search.backend' => Chewy.enabled? ? 'elasticsearch' : 'database'
)
results = from_elasticsearch if Chewy.enabled?
results ||= from_database
span.set_attribute('search.results.count', results.size)
results
end
end
private

View file

@ -38,7 +38,7 @@ module ActiveRecord
end
end
super(direction, migrations, schema_migration, internal_metadata, target_version)
super
end
end

View file

@ -66,3 +66,5 @@ if ENV.keys.any? { |name| name.match?(/OTEL_.*_ENDPOINT/) }
c.service_version = Mastodon::Version.to_s
end
end
MastodonOTELTracer = OpenTelemetry.tracer_provider.tracer('mastodon')

View file

@ -852,7 +852,6 @@ an:
delete: Borrar
edit_preset: Editar aviso predeterminau
empty: Encara no has definiu garra preajuste d'alvertencia.
title: Editar configuración predeterminada d'avisos
webhooks:
add_new: Anyadir endpoint
delete: Eliminar

View file

@ -1013,7 +1013,6 @@ ar:
delete: حذف
edit_preset: تعديل نموذج التحذير
empty: لم تحدد أي إعدادات تحذير مسبقة بعد.
title: إدارة نماذج التحذير
webhooks:
add_new: إضافة نقطة نهاية
delete: حذف

View file

@ -400,8 +400,6 @@ ast:
usable: Pue usase
title: Tendencies
trending: En tendencia
warning_presets:
title: Xestión d'alvertencies preconfiguraes
webhooks:
add_new: Amestar un estremu
delete: Desaniciar

View file

@ -983,7 +983,6 @@ be:
delete: Выдаліць
edit_preset: Рэдагаваць шаблон папярэджання
empty: Вы яшчэ не вызначылі ніякіх шаблонаў папярэджанняў.
title: Кіраванне шаблонамі папярэджанняў
webhooks:
add_new: Дадаць канцавую кропку
delete: Выдаліць

View file

@ -951,7 +951,7 @@ bg:
delete: Изтриване
edit_preset: Редакция на предварителните настройки
empty: Все още няма предварителни настройки за предупрежденията.
title: Управление на предварителните настройки
title: Предупредителни образци
webhooks:
add_new: Добавяне на крайна точка
delete: Изтриване

View file

@ -951,7 +951,7 @@ ca:
delete: Elimina
edit_preset: Edita l'avís predeterminat
empty: Encara no has definit cap preavís.
title: Gestiona les configuracions predefinides dels avisos
title: Predefinicions d'avís
webhooks:
add_new: Afegir extrem
delete: Elimina

View file

@ -548,7 +548,6 @@ ckb:
add_new: زیادکردنی نوێ
delete: سڕینەوە
edit_preset: دەستکاریکردنی ئاگاداری پێشگریمان
title: بەڕێوەبردنی ئاگادارکردنەوە پێش‌سازدان
admin_mailer:
new_pending_account:
body: وردەکاریهەژمارە نوێیەکە لە خوارەوەیە. دەتوانیت ئەم نەرمەکالا پەسەند بکەیت یان ڕەت بکەیتەوە.

View file

@ -510,7 +510,6 @@ co:
add_new: Aghjunghje
delete: Sguassà
edit_preset: Cambià a preselezzione d'avertimentu
title: Amministrà e preselezzione d'avertimentu
admin_mailer:
new_pending_account:
body: I ditagli di u novu contu sò quì sottu. Pudete appruvà o righjittà a dumanda.

View file

@ -984,7 +984,6 @@ cs:
delete: Smazat
edit_preset: Upravit předlohu pro varování
empty: Zatím jste nedefinovali žádné předlohy varování.
title: Spravovat předlohy pro varování
webhooks:
add_new: Přidat koncový bod
delete: Smazat

View file

@ -297,6 +297,7 @@ cy:
update_custom_emoji_html: Mae %{name} wedi diweddaru emoji %{target}
update_domain_block_html: Mae %{name} wedi diweddaru bloc parth %{target}
update_ip_block_html: Mae %{name} wedi newid rheol IP %{target}
update_report_html: Mae %{name} wedi diweddaru adroddiad %{target}
update_status_html: Mae %{name} wedi diweddaru postiad gan %{target}
update_user_role_html: Mae %{name} wedi newid rôl %{target}
deleted_account: cyfrif wedi'i ddileu
@ -1018,7 +1019,7 @@ cy:
delete: Dileu
edit_preset: Golygu rhagosodiad rhybudd
empty: Nid ydych wedi diffinio unrhyw ragosodiadau rhybudd eto.
title: Rheoli rhagosodiadau rhybudd
title: Rhagosodiadau rhybuddion
webhooks:
add_new: Ychwanegu diweddbwynt
delete: Dileu

View file

@ -951,7 +951,7 @@ da:
delete: Slet
edit_preset: Redigér advarselsforvalg
empty: Ingen advarselsforvalg defineret endnu.
title: Håndtérr advarselsforvalg
title: Præindstillinger for advarsel
webhooks:
add_new: Tilføj endepunkt
delete: Slet

View file

@ -951,7 +951,7 @@ de:
delete: Löschen
edit_preset: Warnvorlage bearbeiten
empty: Du hast noch keine Warnvorlagen hinzugefügt.
title: Warnvorlagen verwalten
title: Warnvorlagen
webhooks:
add_new: Endpunkt hinzufügen
delete: Löschen

View file

@ -22,7 +22,7 @@ lt:
action: Patvirtinti el. pašto adresą
action_with_app: Patvirtinti ir grįžti į %{app}
explanation: Šiuo el. pašto adresu sukūrei paskyrą %{host}. Iki jos aktyvavimo liko vienas paspaudimas. Jei tai buvo ne tu, ignoruok šį el. laišką.
explanation_when_pending: Šiuo el. pašto adresu pateikei paraišką pakvietimui į %{host}. Kai patvirtinsi savo el. pašto adresą, mes peržiūrėsime tavo paraišką. Gali prisijungti ir pakeisti savo duomenis arba ištrinti paskyrą, tačiau negalėsi naudotis daugeliu funkcijų, kol tavo paskyra nebus patvirtinta. Jei tavo paraiška bus atmesta, duomenys bus pašalinti, todėl jokių papildomų veiksmų iš tavęs nereikės. Jei tai buvo ne tu, ignoruok šį el. laišką.
explanation_when_pending: Šiuo el. pašto adresu pateikei paraišką pakvietimui į %{host}. Kai patvirtinsi savo el. pašto adresą, mes peržiūrėsime tavo paraišką. Gali prisijungti ir pakeisti savo duomenis arba ištrinti paskyrą, bet negalėsi naudotis daugeliu funkcijų, kol tavo paskyra nebus patvirtinta. Jei tavo paraiška bus atmesta, duomenys bus pašalinti, todėl jokių papildomų veiksmų iš tavęs nereikės. Jei tai buvo ne tu, ignoruok šį el. laišką.
extra_html: Taip pat peržiūrėk <a href="%{terms_path}">serverio taisykles</a> ir <a href="%{policy_path}">mūsų paslaugų teikimo sąlygas</a>.
subject: 'Mastodon: patvirtinimo instrukcijos %{instance}'
title: Patvirtinti el. pašto adresą

View file

@ -903,7 +903,6 @@ el:
delete: Διαγραφή
edit_preset: Ενημέρωση προκαθορισμένης προειδοποίησης
empty: Δεν έχετε ακόμη ορίσει κάποια προκαθορισμένη προειδοποίηση.
title: Διαχείριση προκαθορισμένων προειδοποιήσεων
webhooks:
add_new: Προσθήκη σημείου τερματισμού
delete: Διαγραφή

View file

@ -950,7 +950,6 @@ en-GB:
delete: Delete
edit_preset: Edit warning preset
empty: You haven't defined any warning presets yet.
title: Warning presets
webhooks:
add_new: Add endpoint
delete: Delete

View file

@ -919,7 +919,6 @@ eo:
delete: Forigi
edit_preset: Redakti avertan antaŭagordon
empty: Vi ankoraŭ ne difinis iun ajn antaŭagordon de averto.
title: Administri avertajn antaŭagordojn
webhooks:
add_new: Aldoni finpunkton
delete: Forigi

View file

@ -951,7 +951,7 @@ es-AR:
delete: Eliminar
edit_preset: Editar preajuste de advertencia
empty: Aún no ha definido ningún preajuste de advertencia.
title: Administrar preajustes de advertencia
title: Preajustes de advertencia
webhooks:
add_new: Agregar punto final
delete: Eliminar

View file

@ -951,7 +951,7 @@ es-MX:
delete: Borrar
edit_preset: Editar aviso predeterminado
empty: Aún no has definido ningún preajuste de advertencia.
title: Editar configuración predeterminada de avisos
title: Preajustes de advertencia
webhooks:
add_new: Añadir endpoint
delete: Eliminar

View file

@ -951,7 +951,7 @@ es:
delete: Borrar
edit_preset: Editar aviso predeterminado
empty: Aún no has definido ningún preajuste de advertencia.
title: Editar configuración predeterminada de avisos
title: Preajustes de advertencia
webhooks:
add_new: Añadir endpoint
delete: Eliminar

View file

@ -949,7 +949,6 @@ et:
delete: Kustuta
edit_preset: Hoiatuse eelseadistuse muutmine
empty: Hoiatuste eelseadeid pole defineeritud.
title: Halda hoiatuste eelseadistusi
webhooks:
add_new: Lisa lõpp-punkt
delete: Kustuta

View file

@ -952,7 +952,6 @@ eu:
delete: Ezabatu
edit_preset: Editatu abisu aurre-ezarpena
empty: Ez duzu abisu aurrezarpenik definitu oraindik.
title: Kudeatu abisu aurre-ezarpenak
webhooks:
add_new: Gehitu amaiera-puntua
delete: Ezabatu

View file

@ -808,7 +808,6 @@ fa:
delete: زدودن
edit_preset: ویرایش هشدار پیش‌فرض
empty: هنز هیچ پیش‌تنظیم هشداری را تعریف نکرده‌اید.
title: مدیریت هشدارهای پیش‌فرض
webhooks:
add_new: افزودن نقطهٔ پایانی
delete: حذف

View file

@ -951,7 +951,7 @@ fi:
delete: Poista
edit_preset: Muokkaa varoituksen esiasetusta
empty: Et ole vielä määrittänyt yhtäkään varoitusten esiasetusta.
title: Hallitse varoitusten esiasetuksia
title: Varoituksen esiasetukset
webhooks:
add_new: Lisää päätepiste
delete: Poista

View file

@ -951,7 +951,7 @@ fo:
delete: Strika
edit_preset: Rætta ávaringar-undanstilling
empty: Tú hevur ikki ásett nakrar ávaringar-undanstillingar enn.
title: Stýr ávaringar-undanstillingar
title: Undanstillingar fyri ávaring
webhooks:
add_new: Legg endapunkt afturat
delete: Strika

View file

@ -949,7 +949,6 @@ fr-CA:
delete: Supprimer
edit_preset: Éditer les avertissements prédéfinis
empty: Vous n'avez pas encore créé de paramètres prédéfinis pour les avertissements.
title: Gérer les avertissements prédéfinis
webhooks:
add_new: Ajouter un point de terminaison
delete: Supprimer

View file

@ -949,7 +949,6 @@ fr:
delete: Supprimer
edit_preset: Éditer les avertissements prédéfinis
empty: Vous n'avez pas encore créé de paramètres prédéfinis pour les avertissements.
title: Gérer les avertissements prédéfinis
webhooks:
add_new: Ajouter un point de terminaison
delete: Supprimer

View file

@ -949,7 +949,6 @@ fy:
delete: Fuortsmite
edit_preset: Foarynstelling foar warskôging bewurkje
empty: Jo hawwe noch gjin foarynstellingen foar warskôgingen tafoege.
title: Foarynstellingen foar warskôgingen beheare
webhooks:
add_new: Einpunt tafoegje
delete: Fuortsmite

View file

@ -983,7 +983,6 @@ gd:
delete: Sguab às
edit_preset: Deasaich rabhadh ro-shuidhichte
empty: Cha do mhìnich thu ro-sheataichean rabhaidhean fhathast.
title: Stiùirich na rabhaidhean ro-shuidhichte
webhooks:
add_new: Cuir puing-dheiridh ris
delete: Sguab às

View file

@ -951,7 +951,7 @@ gl:
delete: Eliminar
edit_preset: Editar aviso preestablecido
empty: Non definiches os avisos prestablecidos.
title: Xestionar avisos preestablecidos
title: Preestablecidos de advertencia
webhooks:
add_new: Engadir punto de extremo
delete: Eliminar

View file

@ -985,7 +985,7 @@ he:
delete: למחוק
edit_preset: ערוך/י טקסט מוכן מראש לאזהרה
empty: לא הגדרת עדיין שום טקסט מוכן מראש לאזהרה.
title: ניהול טקסטים מוכנים מראש לאזהרות
title: תצורת אזהרות
webhooks:
add_new: הוספת נקודת קצה
delete: מחיקה

View file

@ -951,7 +951,7 @@ hu:
delete: Törlés
edit_preset: Figyelmeztetés szerkesztése
empty: Nem definiáltál még egyetlen figyelmeztetést sem.
title: Figyelmeztetések
title: Figyelmeztető szövegek
webhooks:
add_new: Végpont hozzáadása
delete: Törlés

View file

@ -951,7 +951,7 @@ ia:
delete: Deler
edit_preset: Rediger aviso predefinite
empty: Tu non ha ancora definite alcun avisos predefinite.
title: Gerer avisos predefinite
title: Predefinitiones de avisos
webhooks:
add_new: Adder terminal
delete: Deler

View file

@ -831,7 +831,6 @@ id:
delete: Hapus
edit_preset: Sunting preset peringatan
empty: Anda belum mendefinisikan peringatan apapun.
title: Kelola preset peringatan
webhooks:
add_new: Tambah titik akhir
delete: Hapus

View file

@ -950,7 +950,6 @@ ie:
delete: Deleter
edit_preset: Modificar prefiguration de avise
empty: Vu ancor ha definit null prefigurationes de avise.
title: Modificar prefigurationes de avise
webhooks:
add_new: Adjunter punctu terminal
delete: Deleter

View file

@ -928,7 +928,6 @@ io:
delete: Efacez
edit_preset: Modifikez avertfixito
empty: Vu ne fixis irga avertfixito til nun.
title: Jerez avertfixiti
webhooks:
add_new: Insertez finpunto
delete: Efacez

View file

@ -953,7 +953,7 @@ is:
delete: Eyða
edit_preset: Breyta forstilltri aðvörun
empty: Þú hefur ekki enn skilgreint neinar aðvaranaforstillingar.
title: Sýsla með forstilltar aðvaranir
title: Forstilltar aðvaranir
webhooks:
add_new: Bæta við endapunkti
delete: Eyða

View file

@ -951,7 +951,7 @@ it:
delete: Cancella
edit_preset: Modifica avviso predefinito
empty: Non hai ancora definito alcun avviso preimpostato.
title: Gestisci avvisi predefiniti
title: Preimpostazioni di avviso
webhooks:
add_new: Aggiungi endpoint
delete: Elimina

View file

@ -939,7 +939,6 @@ ja:
delete: 削除
edit_preset: プリセット警告文を編集
empty: まだプリセット警告文が作成されていません。
title: プリセット警告文を管理
webhooks:
add_new: エンドポイントを追加
delete: 削除

View file

@ -299,7 +299,6 @@ kk:
add_new: Add nеw
delete: Deletе
edit_preset: Edit warning prеset
title: Manage warning presеts
admin_mailer:
new_pending_account:
body: Жаңа есептік жазба туралы мәліметтер төменде берілген. Бұл қолданбаны мақұлдауыңызға немесе қабылдамауыңызға болады.

Some files were not shown because too many files have changed in this diff Show more