((resolve) => {
- resolve(simulateModifiedApiResponse(media, params));
- });
- }
-
- return apiUpdateMedia(id, params);
- },
- (media: SimulatedMediaAttachmentJSON) => {
- return {
- media,
- attached: typeof media.unattached !== 'undefined' && !media.unattached,
- };
- },
- {
- useLoadingBar: false,
- },
-);
diff --git a/app/javascript/mastodon/actions/domain_blocks.js b/app/javascript/mastodon/actions/domain_blocks.js
index 279ec1bef7..727f800af3 100644
--- a/app/javascript/mastodon/actions/domain_blocks.js
+++ b/app/javascript/mastodon/actions/domain_blocks.js
@@ -12,6 +12,14 @@ export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL';
export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST';
export const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL';
+export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST';
+export const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS';
+export const DOMAIN_BLOCKS_FETCH_FAIL = 'DOMAIN_BLOCKS_FETCH_FAIL';
+
+export const DOMAIN_BLOCKS_EXPAND_REQUEST = 'DOMAIN_BLOCKS_EXPAND_REQUEST';
+export const DOMAIN_BLOCKS_EXPAND_SUCCESS = 'DOMAIN_BLOCKS_EXPAND_SUCCESS';
+export const DOMAIN_BLOCKS_EXPAND_FAIL = 'DOMAIN_BLOCKS_EXPAND_FAIL';
+
export function blockDomain(domain) {
return (dispatch, getState) => {
dispatch(blockDomainRequest(domain));
@@ -71,6 +79,80 @@ export function unblockDomainFail(domain, error) {
};
}
+export function fetchDomainBlocks() {
+ return (dispatch) => {
+ dispatch(fetchDomainBlocksRequest());
+
+ api().get('/api/v1/domain_blocks').then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null));
+ }).catch(err => {
+ dispatch(fetchDomainBlocksFail(err));
+ });
+ };
+}
+
+export function fetchDomainBlocksRequest() {
+ return {
+ type: DOMAIN_BLOCKS_FETCH_REQUEST,
+ };
+}
+
+export function fetchDomainBlocksSuccess(domains, next) {
+ return {
+ type: DOMAIN_BLOCKS_FETCH_SUCCESS,
+ domains,
+ next,
+ };
+}
+
+export function fetchDomainBlocksFail(error) {
+ return {
+ type: DOMAIN_BLOCKS_FETCH_FAIL,
+ error,
+ };
+}
+
+export function expandDomainBlocks() {
+ return (dispatch, getState) => {
+ const url = getState().getIn(['domain_lists', 'blocks', 'next']);
+
+ if (!url) {
+ return;
+ }
+
+ dispatch(expandDomainBlocksRequest());
+
+ api().get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(expandDomainBlocksSuccess(response.data, next ? next.uri : null));
+ }).catch(err => {
+ dispatch(expandDomainBlocksFail(err));
+ });
+ };
+}
+
+export function expandDomainBlocksRequest() {
+ return {
+ type: DOMAIN_BLOCKS_EXPAND_REQUEST,
+ };
+}
+
+export function expandDomainBlocksSuccess(domains, next) {
+ return {
+ type: DOMAIN_BLOCKS_EXPAND_SUCCESS,
+ domains,
+ next,
+ };
+}
+
+export function expandDomainBlocksFail(error) {
+ return {
+ type: DOMAIN_BLOCKS_EXPAND_FAIL,
+ error,
+ };
+}
+
export const initDomainBlockModal = account => dispatch => dispatch(openModal({
modalType: 'DOMAIN_BLOCK',
modalProps: {
diff --git a/app/javascript/mastodon/actions/dropdown_menu.ts b/app/javascript/mastodon/actions/dropdown_menu.ts
index d9d395ba33..3694df1ae0 100644
--- a/app/javascript/mastodon/actions/dropdown_menu.ts
+++ b/app/javascript/mastodon/actions/dropdown_menu.ts
@@ -1,11 +1,11 @@
import { createAction } from '@reduxjs/toolkit';
export const openDropdownMenu = createAction<{
- id: number;
+ id: string;
keyboard: boolean;
- scrollKey?: string;
+ scrollKey: string;
}>('dropdownMenu/open');
-export const closeDropdownMenu = createAction<{ id: number }>(
+export const closeDropdownMenu = createAction<{ id: string }>(
'dropdownMenu/close',
);
diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js
index fc165b1a1f..ebf58b761a 100644
--- a/app/javascript/mastodon/actions/importer/index.js
+++ b/app/javascript/mastodon/actions/importer/index.js
@@ -1,12 +1,10 @@
-import { createPollFromServerJSON } from 'mastodon/models/poll';
-
import { importAccounts } from '../accounts_typed';
-import { normalizeStatus } from './normalizer';
-import { importPolls } from './polls';
+import { normalizeStatus, normalizePoll } from './normalizer';
export const STATUS_IMPORT = 'STATUS_IMPORT';
export const STATUSES_IMPORT = 'STATUSES_IMPORT';
+export const POLLS_IMPORT = 'POLLS_IMPORT';
export const FILTERS_IMPORT = 'FILTERS_IMPORT';
function pushUnique(array, object) {
@@ -27,6 +25,10 @@ export function importFilters(filters) {
return { type: FILTERS_IMPORT, filters };
}
+export function importPolls(polls) {
+ return { type: POLLS_IMPORT, polls };
+}
+
export function importFetchedAccount(account) {
return importFetchedAccounts([account]);
}
@@ -75,7 +77,7 @@ export function importFetchedStatuses(statuses) {
}
if (status.poll?.id) {
- pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls[status.poll.id]));
+ pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id])));
}
if (status.card) {
@@ -85,9 +87,15 @@ export function importFetchedStatuses(statuses) {
statuses.forEach(processStatus);
- dispatch(importPolls({ polls }));
+ dispatch(importPolls(polls));
dispatch(importFetchedAccounts(accounts));
dispatch(importStatuses(normalStatuses));
dispatch(importFilters(filters));
};
}
+
+export function importFetchedPoll(poll) {
+ return (dispatch, getState) => {
+ dispatch(importPolls([normalizePoll(poll, getState().getIn(['polls', poll.id]))]));
+ };
+}
diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js
index b643cf5613..d9e9fef0c6 100644
--- a/app/javascript/mastodon/actions/importer/normalizer.js
+++ b/app/javascript/mastodon/actions/importer/normalizer.js
@@ -1,12 +1,15 @@
import escapeTextContentForBrowser from 'escape-html';
-import { makeEmojiMap } from 'mastodon/models/custom_emoji';
-
import emojify from '../../features/emoji/emoji';
import { expandSpoilers, me } from '../../initial_state';
const domParser = new DOMParser();
+const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => {
+ obj[`:${emoji.shortcode}:`] = emoji;
+ return obj;
+}, {});
+
export function searchTextFromRawStatus (status) {
const spoilerText = status.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/
/g, '\n').replace(/<\/p>/g, '\n\n');
@@ -137,6 +140,38 @@ export function normalizeStatusTranslation(translation, status) {
return normalTranslation;
}
+export function normalizePoll(poll, normalOldPoll) {
+ const normalPoll = { ...poll };
+ const emojiMap = makeEmojiMap(poll.emojis);
+
+ normalPoll.options = poll.options.map((option, index) => {
+ const normalOption = {
+ ...option,
+ voted: poll.own_votes && poll.own_votes.includes(index),
+ titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
+ };
+
+ if (normalOldPoll && normalOldPoll.getIn(['options', index, 'title']) === option.title) {
+ normalOption.translation = normalOldPoll.getIn(['options', index, 'translation']);
+ }
+
+ return normalOption;
+ });
+
+ return normalPoll;
+}
+
+export function normalizePollOptionTranslation(translation, poll) {
+ const emojiMap = makeEmojiMap(poll.get('emojis').toJS());
+
+ const normalTranslation = {
+ ...translation,
+ titleHtml: emojify(escapeTextContentForBrowser(translation.title), emojiMap),
+ };
+
+ return normalTranslation;
+}
+
export function normalizeAnnouncement(announcement) {
const normalAnnouncement = { ...announcement };
const emojiMap = makeEmojiMap(normalAnnouncement.emojis);
diff --git a/app/javascript/mastodon/actions/importer/polls.ts b/app/javascript/mastodon/actions/importer/polls.ts
deleted file mode 100644
index 5bbe7d57d6..0000000000
--- a/app/javascript/mastodon/actions/importer/polls.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { createAction } from '@reduxjs/toolkit';
-
-import type { Poll } from 'mastodon/models/poll';
-
-export const importPolls = createAction<{ polls: Poll[] }>(
- 'poll/importMultiple',
-);
diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js
index 7b6dd22041..a7cc1a9329 100644
--- a/app/javascript/mastodon/actions/lists.js
+++ b/app/javascript/mastodon/actions/lists.js
@@ -2,6 +2,9 @@
import api from '../api';
+import { showAlertForError } from './alerts';
+import { importFetchedAccounts } from './importer';
+
export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
@@ -10,10 +13,45 @@ export const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST';
export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS';
export const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL';
+export const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE';
+export const LIST_EDITOR_RESET = 'LIST_EDITOR_RESET';
+export const LIST_EDITOR_SETUP = 'LIST_EDITOR_SETUP';
+
+export const LIST_CREATE_REQUEST = 'LIST_CREATE_REQUEST';
+export const LIST_CREATE_SUCCESS = 'LIST_CREATE_SUCCESS';
+export const LIST_CREATE_FAIL = 'LIST_CREATE_FAIL';
+
+export const LIST_UPDATE_REQUEST = 'LIST_UPDATE_REQUEST';
+export const LIST_UPDATE_SUCCESS = 'LIST_UPDATE_SUCCESS';
+export const LIST_UPDATE_FAIL = 'LIST_UPDATE_FAIL';
+
export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST';
export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS';
export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL';
+export const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST';
+export const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS';
+export const LIST_ACCOUNTS_FETCH_FAIL = 'LIST_ACCOUNTS_FETCH_FAIL';
+
+export const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE';
+export const LIST_EDITOR_SUGGESTIONS_READY = 'LIST_EDITOR_SUGGESTIONS_READY';
+export const LIST_EDITOR_SUGGESTIONS_CLEAR = 'LIST_EDITOR_SUGGESTIONS_CLEAR';
+
+export const LIST_EDITOR_ADD_REQUEST = 'LIST_EDITOR_ADD_REQUEST';
+export const LIST_EDITOR_ADD_SUCCESS = 'LIST_EDITOR_ADD_SUCCESS';
+export const LIST_EDITOR_ADD_FAIL = 'LIST_EDITOR_ADD_FAIL';
+
+export const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST';
+export const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS';
+export const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL';
+
+export const LIST_ADDER_RESET = 'LIST_ADDER_RESET';
+export const LIST_ADDER_SETUP = 'LIST_ADDER_SETUP';
+
+export const LIST_ADDER_LISTS_FETCH_REQUEST = 'LIST_ADDER_LISTS_FETCH_REQUEST';
+export const LIST_ADDER_LISTS_FETCH_SUCCESS = 'LIST_ADDER_LISTS_FETCH_SUCCESS';
+export const LIST_ADDER_LISTS_FETCH_FAIL = 'LIST_ADDER_LISTS_FETCH_FAIL';
+
export const fetchList = id => (dispatch, getState) => {
if (getState().getIn(['lists', id])) {
return;
@@ -64,6 +102,94 @@ export const fetchListsFail = error => ({
error,
});
+export const submitListEditor = shouldReset => (dispatch, getState) => {
+ const listId = getState().getIn(['listEditor', 'listId']);
+ const title = getState().getIn(['listEditor', 'title']);
+
+ if (listId === null) {
+ dispatch(createList(title, shouldReset));
+ } else {
+ dispatch(updateList(listId, title, shouldReset));
+ }
+};
+
+export const setupListEditor = listId => (dispatch, getState) => {
+ dispatch({
+ type: LIST_EDITOR_SETUP,
+ list: getState().getIn(['lists', listId]),
+ });
+
+ dispatch(fetchListAccounts(listId));
+};
+
+export const changeListEditorTitle = value => ({
+ type: LIST_EDITOR_TITLE_CHANGE,
+ value,
+});
+
+export const createList = (title, shouldReset) => (dispatch) => {
+ dispatch(createListRequest());
+
+ api().post('/api/v1/lists', { title }).then(({ data }) => {
+ dispatch(createListSuccess(data));
+
+ if (shouldReset) {
+ dispatch(resetListEditor());
+ }
+ }).catch(err => dispatch(createListFail(err)));
+};
+
+export const createListRequest = () => ({
+ type: LIST_CREATE_REQUEST,
+});
+
+export const createListSuccess = list => ({
+ type: LIST_CREATE_SUCCESS,
+ list,
+});
+
+export const createListFail = error => ({
+ type: LIST_CREATE_FAIL,
+ error,
+});
+
+export const updateList = (id, title, shouldReset, isExclusive, replies_policy, notify) => (dispatch) => {
+ dispatch(updateListRequest(id));
+
+ api().put(`/api/v1/lists/${id}`, {
+ title,
+ replies_policy,
+ exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive,
+ notify: typeof notify === 'undefined' ? undefined : !!notify,
+ }).then(({ data }) => {
+ dispatch(updateListSuccess(data));
+
+ if (shouldReset) {
+ dispatch(resetListEditor());
+ }
+ }).catch(err => dispatch(updateListFail(id, err)));
+};
+
+export const updateListRequest = id => ({
+ type: LIST_UPDATE_REQUEST,
+ id,
+});
+
+export const updateListSuccess = list => ({
+ type: LIST_UPDATE_SUCCESS,
+ list,
+});
+
+export const updateListFail = (id, error) => ({
+ type: LIST_UPDATE_FAIL,
+ id,
+ error,
+});
+
+export const resetListEditor = () => ({
+ type: LIST_EDITOR_RESET,
+});
+
export const deleteList = id => (dispatch) => {
dispatch(deleteListRequest(id));
@@ -87,3 +213,166 @@ export const deleteListFail = (id, error) => ({
id,
error,
});
+
+export const fetchListAccounts = listId => (dispatch) => {
+ dispatch(fetchListAccountsRequest(listId));
+
+ api().get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => {
+ dispatch(importFetchedAccounts(data));
+ dispatch(fetchListAccountsSuccess(listId, data));
+ }).catch(err => dispatch(fetchListAccountsFail(listId, err)));
+};
+
+export const fetchListAccountsRequest = id => ({
+ type: LIST_ACCOUNTS_FETCH_REQUEST,
+ id,
+});
+
+export const fetchListAccountsSuccess = (id, accounts, next) => ({
+ type: LIST_ACCOUNTS_FETCH_SUCCESS,
+ id,
+ accounts,
+ next,
+});
+
+export const fetchListAccountsFail = (id, error) => ({
+ type: LIST_ACCOUNTS_FETCH_FAIL,
+ id,
+ error,
+});
+
+export const fetchListSuggestions = q => (dispatch) => {
+ const params = {
+ q,
+ resolve: false,
+ following: true,
+ };
+
+ api().get('/api/v1/accounts/search', { params }).then(({ data }) => {
+ dispatch(importFetchedAccounts(data));
+ dispatch(fetchListSuggestionsReady(q, data));
+ }).catch(error => dispatch(showAlertForError(error)));
+};
+
+export const fetchListSuggestionsReady = (query, accounts) => ({
+ type: LIST_EDITOR_SUGGESTIONS_READY,
+ query,
+ accounts,
+});
+
+export const clearListSuggestions = () => ({
+ type: LIST_EDITOR_SUGGESTIONS_CLEAR,
+});
+
+export const changeListSuggestions = value => ({
+ type: LIST_EDITOR_SUGGESTIONS_CHANGE,
+ value,
+});
+
+export const addToListEditor = accountId => (dispatch, getState) => {
+ dispatch(addToList(getState().getIn(['listEditor', 'listId']), accountId));
+};
+
+export const addToList = (listId, accountId) => (dispatch) => {
+ dispatch(addToListRequest(listId, accountId));
+
+ api().post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] })
+ .then(() => dispatch(addToListSuccess(listId, accountId)))
+ .catch(err => dispatch(addToListFail(listId, accountId, err)));
+};
+
+export const addToListRequest = (listId, accountId) => ({
+ type: LIST_EDITOR_ADD_REQUEST,
+ listId,
+ accountId,
+});
+
+export const addToListSuccess = (listId, accountId) => ({
+ type: LIST_EDITOR_ADD_SUCCESS,
+ listId,
+ accountId,
+});
+
+export const addToListFail = (listId, accountId, error) => ({
+ type: LIST_EDITOR_ADD_FAIL,
+ listId,
+ accountId,
+ error,
+});
+
+export const removeFromListEditor = accountId => (dispatch, getState) => {
+ dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), accountId));
+};
+
+export const removeFromList = (listId, accountId) => (dispatch) => {
+ dispatch(removeFromListRequest(listId, accountId));
+
+ api().delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
+ .then(() => dispatch(removeFromListSuccess(listId, accountId)))
+ .catch(err => dispatch(removeFromListFail(listId, accountId, err)));
+};
+
+export const removeFromListRequest = (listId, accountId) => ({
+ type: LIST_EDITOR_REMOVE_REQUEST,
+ listId,
+ accountId,
+});
+
+export const removeFromListSuccess = (listId, accountId) => ({
+ type: LIST_EDITOR_REMOVE_SUCCESS,
+ listId,
+ accountId,
+});
+
+export const removeFromListFail = (listId, accountId, error) => ({
+ type: LIST_EDITOR_REMOVE_FAIL,
+ listId,
+ accountId,
+ error,
+});
+
+export const resetListAdder = () => ({
+ type: LIST_ADDER_RESET,
+});
+
+export const setupListAdder = accountId => (dispatch, getState) => {
+ dispatch({
+ type: LIST_ADDER_SETUP,
+ account: getState().getIn(['accounts', accountId]),
+ });
+ dispatch(fetchLists());
+ dispatch(fetchAccountLists(accountId));
+};
+
+export const fetchAccountLists = accountId => (dispatch) => {
+ dispatch(fetchAccountListsRequest(accountId));
+
+ api().get(`/api/v1/accounts/${accountId}/lists`)
+ .then(({ data }) => dispatch(fetchAccountListsSuccess(accountId, data)))
+ .catch(err => dispatch(fetchAccountListsFail(accountId, err)));
+};
+
+export const fetchAccountListsRequest = id => ({
+ type:LIST_ADDER_LISTS_FETCH_REQUEST,
+ id,
+});
+
+export const fetchAccountListsSuccess = (id, lists) => ({
+ type: LIST_ADDER_LISTS_FETCH_SUCCESS,
+ id,
+ lists,
+});
+
+export const fetchAccountListsFail = (id, err) => ({
+ type: LIST_ADDER_LISTS_FETCH_FAIL,
+ id,
+ err,
+});
+
+export const addToListAdder = listId => (dispatch, getState) => {
+ dispatch(addToList(listId, getState().getIn(['listAdder', 'accountId'])));
+};
+
+export const removeFromListAdder = listId => (dispatch, getState) => {
+ dispatch(removeFromList(listId, getState().getIn(['listAdder', 'accountId'])));
+};
diff --git a/app/javascript/mastodon/actions/lists_typed.ts b/app/javascript/mastodon/actions/lists_typed.ts
deleted file mode 100644
index eca051f52c..0000000000
--- a/app/javascript/mastodon/actions/lists_typed.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { apiCreate, apiUpdate } from 'mastodon/api/lists';
-import type { List } from 'mastodon/models/list';
-import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
-
-export const createList = createDataLoadingThunk(
- 'list/create',
- (list: Partial) => apiCreate(list),
-);
-
-// Kmyblue tracking marker: copied antenna, circle, bookmark_category
-
-export const updateList = createDataLoadingThunk(
- 'list/update',
- (list: Partial) => apiUpdate(list),
-);
-
-// Kmyblue tracking marker: copied antenna, circle, bookmark_category
diff --git a/app/javascript/mastodon/actions/modal.ts b/app/javascript/mastodon/actions/modal.ts
index 49af176a11..ab03e46765 100644
--- a/app/javascript/mastodon/actions/modal.ts
+++ b/app/javascript/mastodon/actions/modal.ts
@@ -9,7 +9,6 @@ export type ModalType = keyof typeof MODAL_COMPONENTS;
interface OpenModalPayload {
modalType: ModalType;
modalProps: ModalProps;
- previousModalProps?: ModalProps;
}
export const openModal = createAction('MODAL_OPEN');
diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts
index c7b192accc..4f24c1c106 100644
--- a/app/javascript/mastodon/actions/notification_groups.ts
+++ b/app/javascript/mastodon/actions/notification_groups.ts
@@ -8,15 +8,13 @@ import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import type {
ApiNotificationGroupJSON,
ApiNotificationJSON,
- NotificationType,
} from 'mastodon/api_types/notifications';
import { allNotificationTypes } from 'mastodon/api_types/notifications';
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
-import { enableEmojiReaction, usePendingItems } from 'mastodon/initial_state';
+import { usePendingItems } from 'mastodon/initial_state';
import type { NotificationGap } from 'mastodon/reducers/notification_groups';
import {
selectSettingsNotificationsExcludedTypes,
- selectSettingsNotificationsGroupFollows,
selectSettingsNotificationsQuickFilterActive,
selectSettingsNotificationsShows,
} from 'mastodon/selectors/settings';
@@ -37,15 +35,9 @@ function excludeAllTypesExcept(filter: string) {
function getExcludedTypes(state: RootState) {
const activeFilter = selectSettingsNotificationsQuickFilterActive(state);
- const types =
- activeFilter === 'all'
- ? selectSettingsNotificationsExcludedTypes(state)
- : excludeAllTypesExcept(activeFilter);
- if (!enableEmojiReaction && !types.includes('emoji_reaction')) {
- types.push('emoji_reaction');
- }
-
- return types;
+ return activeFilter === 'all'
+ ? selectSettingsNotificationsExcludedTypes(state)
+ : excludeAllTypesExcept(activeFilter);
}
function dispatchAssociatedRecords(
@@ -76,19 +68,21 @@ function dispatchAssociatedRecords(
dispatch(importFetchedStatuses(fetchedStatuses));
}
-function selectNotificationGroupedTypes(state: RootState) {
- const types: NotificationType[] = ['favourite', 'reblog', 'emoji_reaction'];
+const supportedGroupedNotificationTypes = [
+ 'favourite',
+ 'reblog',
+ 'emoji_reaction',
+];
- if (selectSettingsNotificationsGroupFollows(state)) types.push('follow');
-
- return types;
+export function shouldGroupNotificationType(type: string) {
+ return supportedGroupedNotificationTypes.includes(type);
}
export const fetchNotifications = createDataLoadingThunk(
'notificationGroups/fetch',
async (_params, { getState }) =>
apiFetchNotificationGroups({
- grouped_types: selectNotificationGroupedTypes(getState()),
+ grouped_types: supportedGroupedNotificationTypes,
exclude_types: getExcludedTypes(getState()),
}),
({ notifications, accounts, statuses }, { dispatch }) => {
@@ -112,7 +106,7 @@ export const fetchNotificationsGap = createDataLoadingThunk(
'notificationGroups/fetchGap',
async (params: { gap: NotificationGap }, { getState }) =>
apiFetchNotificationGroups({
- grouped_types: selectNotificationGroupedTypes(getState()),
+ grouped_types: supportedGroupedNotificationTypes,
max_id: params.gap.maxId,
exclude_types: getExcludedTypes(getState()),
}),
@@ -129,7 +123,7 @@ export const pollRecentNotifications = createDataLoadingThunk(
'notificationGroups/pollRecentNotifications',
async (_params, { getState }) => {
return apiFetchNotificationGroups({
- grouped_types: selectNotificationGroupedTypes(getState()),
+ grouped_types: supportedGroupedNotificationTypes,
max_id: undefined,
exclude_types: getExcludedTypes(getState()),
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones
@@ -147,9 +141,6 @@ export const pollRecentNotifications = createDataLoadingThunk(
return { notifications };
},
- {
- useLoadingBar: false,
- },
);
export const processNewNotificationForGroups = createAppAsyncThunk(
@@ -161,7 +152,7 @@ export const processNewNotificationForGroups = createAppAsyncThunk(
const showInColumn =
activeFilter === 'all'
- ? notificationShows[notification.type] !== false
+ ? notificationShows[notification.type]
: activeFilter === notification.type;
if (!showInColumn) return;
@@ -181,10 +172,7 @@ export const processNewNotificationForGroups = createAppAsyncThunk(
dispatchAssociatedRecords(dispatch, [notification]);
- return {
- notification,
- groupedTypes: selectNotificationGroupedTypes(state),
- };
+ return notification;
},
);
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index 87b842e51f..f8b2aa13a4 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -1,25 +1,57 @@
import { IntlMessageFormat } from 'intl-messageformat';
import { defineMessages } from 'react-intl';
+import { List as ImmutableList } from 'immutable';
+
+import { compareId } from 'mastodon/compare_id';
+import { enableEmojiReaction, usePendingItems as preferPendingItems } from 'mastodon/initial_state';
+
+import api, { getLinks } from '../api';
import { unescapeHTML } from '../utils/html';
import { requestNotificationPermission } from '../utils/notifications';
import { fetchFollowRequests } from './accounts';
import {
importFetchedAccount,
+ importFetchedAccounts,
+ importFetchedStatus,
+ importFetchedStatuses,
} from './importer';
import { submitMarkers } from './markers';
import { notificationsUpdate } from "./notifications_typed";
import { register as registerPushNotifications } from './push_notifications';
+import { saveSettings } from './settings';
import { STATUS_EMOJI_REACTION_UPDATE } from './statuses';
export * from "./notifications_typed";
+export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
+
+export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
+export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
+export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
+
export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
+export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
+export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
+
+export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT';
+export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
+
+export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
+
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';
+export const NOTIFICATION_REQUESTS_ACCEPT_REQUEST = 'NOTIFICATION_REQUESTS_ACCEPT_REQUEST';
+export const NOTIFICATION_REQUESTS_ACCEPT_SUCCESS = 'NOTIFICATION_REQUESTS_ACCEPT_SUCCESS';
+export const NOTIFICATION_REQUESTS_ACCEPT_FAIL = 'NOTIFICATION_REQUESTS_ACCEPT_FAIL';
+
+export const NOTIFICATION_REQUESTS_DISMISS_REQUEST = 'NOTIFICATION_REQUESTS_DISMISS_REQUEST';
+export const NOTIFICATION_REQUESTS_DISMISS_SUCCESS = 'NOTIFICATION_REQUESTS_DISMISS_SUCCESS';
+export const NOTIFICATION_REQUESTS_DISMISS_FAIL = 'NOTIFICATION_REQUESTS_DISMISS_FAIL';
+
const messages = defineMessages({
// mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
@@ -37,6 +69,10 @@ const messages = defineMessages({
message_update: { id: 'notification.update', defaultMessage: '{name} edited a post' },
});
+export const loadPending = () => ({
+ type: NOTIFICATIONS_LOAD_PENDING,
+});
+
export function updateEmojiReactions(emoji_reaction) {
return (dispatch) =>
dispatch({
@@ -47,6 +83,8 @@ export function updateEmojiReactions(emoji_reaction) {
export function updateNotifications(notification, intlMessages, intlLocale) {
return (dispatch, getState) => {
+ const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
+ const showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : activeFilter === notification.type;
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
@@ -55,7 +93,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
if (['mention', 'status'].includes(notification.type) && notification.status.filtered) {
const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications'));
- if (filters.some(result => result.filter.filter_action === 'hide')) {
+ if (filters.some(result => result.filter.filter_action_ex === 'hide')) {
return;
}
@@ -68,9 +106,25 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
dispatch(submitMarkers());
- // `notificationsUpdate` is still used in `user_lists` and `relationships` reducers
- dispatch(importFetchedAccount(notification.account));
- dispatch(notificationsUpdate({ notification, playSound: playSound && !filtered}));
+ if (showInColumn) {
+ dispatch(importFetchedAccount(notification.account));
+
+ if (notification.status) {
+ dispatch(importFetchedStatus(notification.status));
+ }
+
+ if (notification.report) {
+ dispatch(importFetchedAccount(notification.report.target_account));
+ }
+
+
+ dispatch(notificationsUpdate({ notification, preferPendingItems, playSound: playSound && !filtered}));
+ } else if (playSound && !filtered) {
+ dispatch({
+ type: NOTIFICATIONS_UPDATE_NOOP,
+ meta: { sound: 'boop' },
+ });
+ }
// Desktop notifications
if (typeof window.Notification !== 'undefined' && showAlert && !filtered) {
@@ -91,8 +145,149 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
};
}
+const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
+
+const excludeTypesFromFilter = filter => {
+ const allTypes = ImmutableList([
+ 'follow',
+ 'follow_request',
+ 'favourite',
+ 'emoji_reaction',
+ 'reblog',
+ 'status_reference',
+ 'mention',
+ 'poll',
+ 'status',
+ 'list_status',
+ 'update',
+ 'admin.sign_up',
+ 'admin.report',
+ ]);
+
+ return allTypes.filterNot(item => item === filter).toJS();
+};
+
const noOp = () => {};
+let expandNotificationsController = new AbortController();
+
+export function expandNotifications({ maxId = undefined, forceLoad = false }) {
+ return async (dispatch, getState) => {
+ const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
+ const notifications = getState().get('notifications');
+ const isLoadingMore = !!maxId;
+
+ if (notifications.get('isLoading')) {
+ if (forceLoad) {
+ expandNotificationsController.abort();
+ expandNotificationsController = new AbortController();
+ } else {
+ return;
+ }
+ }
+
+ let exclude_types = activeFilter === 'all'
+ ? excludeTypesFromSettings(getState())
+ : excludeTypesFromFilter(activeFilter);
+ if (!enableEmojiReaction && !exclude_types.includes('emoji_reaction')) {
+ exclude_types.push('emoji_reaction');
+ }
+
+ const params = {
+ max_id: maxId,
+ exclude_types,
+ };
+
+ if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) {
+ const a = notifications.getIn(['pendingItems', 0, 'id']);
+ const b = notifications.getIn(['items', 0, 'id']);
+
+ if (a && b && compareId(a, b) > 0) {
+ params.since_id = a;
+ } else {
+ params.since_id = b || a;
+ }
+ }
+
+ const isLoadingRecent = !!params.since_id;
+
+ dispatch(expandNotificationsRequest(isLoadingMore));
+
+ try {
+ const response = await api().get('/api/v1/notifications', { params, signal: expandNotificationsController.signal });
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+ dispatch(importFetchedAccounts(response.data.map(item => item.account)));
+ dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
+ dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
+
+ dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
+ dispatch(submitMarkers());
+ } catch(error) {
+ dispatch(expandNotificationsFail(error, isLoadingMore));
+ }
+ };
+}
+
+export function expandNotificationsRequest(isLoadingMore) {
+ return {
+ type: NOTIFICATIONS_EXPAND_REQUEST,
+ skipLoading: !isLoadingMore,
+ };
+}
+
+export function expandNotificationsSuccess(notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) {
+ return {
+ type: NOTIFICATIONS_EXPAND_SUCCESS,
+ notifications,
+ next,
+ isLoadingRecent: isLoadingRecent,
+ usePendingItems,
+ skipLoading: !isLoadingMore,
+ };
+}
+
+export function expandNotificationsFail(error, isLoadingMore) {
+ return {
+ type: NOTIFICATIONS_EXPAND_FAIL,
+ error,
+ skipLoading: !isLoadingMore,
+ skipAlert: !isLoadingMore || error.name === 'AbortError',
+ };
+}
+
+export function scrollTopNotifications(top) {
+ return {
+ type: NOTIFICATIONS_SCROLL_TOP,
+ top,
+ };
+}
+
+export function setFilter (filterType) {
+ return dispatch => {
+ dispatch({
+ type: NOTIFICATIONS_FILTER_SET,
+ path: ['notifications', 'quickFilter', 'active'],
+ value: filterType,
+ });
+ dispatch(expandNotifications({ forceLoad: true }));
+ dispatch(saveSettings());
+ };
+}
+
+export const mountNotifications = () => ({
+ type: NOTIFICATIONS_MOUNT,
+});
+
+export const unmountNotifications = () => ({
+ type: NOTIFICATIONS_UNMOUNT,
+});
+
+
+export const markNotificationsAsRead = () => ({
+ type: NOTIFICATIONS_MARK_AS_READ,
+});
+
// Browser support
export function setupBrowserNotifications() {
return dispatch => {
diff --git a/app/javascript/mastodon/actions/notifications_migration.tsx b/app/javascript/mastodon/actions/notifications_migration.tsx
new file mode 100644
index 0000000000..cd9f5ca3d6
--- /dev/null
+++ b/app/javascript/mastodon/actions/notifications_migration.tsx
@@ -0,0 +1,10 @@
+import { createAppAsyncThunk } from 'mastodon/store';
+
+import { fetchNotifications } from './notification_groups';
+
+export const initializeNotifications = createAppAsyncThunk(
+ 'notifications/initialize',
+ (_, { dispatch }) => {
+ void dispatch(fetchNotifications());
+ },
+);
diff --git a/app/javascript/mastodon/actions/notifications_typed.ts b/app/javascript/mastodon/actions/notifications_typed.ts
index 3eb1230666..88d942d45e 100644
--- a/app/javascript/mastodon/actions/notifications_typed.ts
+++ b/app/javascript/mastodon/actions/notifications_typed.ts
@@ -9,6 +9,7 @@ export const notificationsUpdate = createAction(
...args
}: {
notification: ApiNotificationJSON;
+ usePendingItems: boolean;
playSound: boolean;
}) => ({
payload: args,
diff --git a/app/javascript/mastodon/actions/polls.js b/app/javascript/mastodon/actions/polls.js
new file mode 100644
index 0000000000..aa49341444
--- /dev/null
+++ b/app/javascript/mastodon/actions/polls.js
@@ -0,0 +1,61 @@
+import api from '../api';
+
+import { importFetchedPoll } from './importer';
+
+export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST';
+export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS';
+export const POLL_VOTE_FAIL = 'POLL_VOTE_FAIL';
+
+export const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST';
+export const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS';
+export const POLL_FETCH_FAIL = 'POLL_FETCH_FAIL';
+
+export const vote = (pollId, choices) => (dispatch) => {
+ dispatch(voteRequest());
+
+ api().post(`/api/v1/polls/${pollId}/votes`, { choices })
+ .then(({ data }) => {
+ dispatch(importFetchedPoll(data));
+ dispatch(voteSuccess(data));
+ })
+ .catch(err => dispatch(voteFail(err)));
+};
+
+export const fetchPoll = pollId => (dispatch) => {
+ dispatch(fetchPollRequest());
+
+ api().get(`/api/v1/polls/${pollId}`)
+ .then(({ data }) => {
+ dispatch(importFetchedPoll(data));
+ dispatch(fetchPollSuccess(data));
+ })
+ .catch(err => dispatch(fetchPollFail(err)));
+};
+
+export const voteRequest = () => ({
+ type: POLL_VOTE_REQUEST,
+});
+
+export const voteSuccess = poll => ({
+ type: POLL_VOTE_SUCCESS,
+ poll,
+});
+
+export const voteFail = error => ({
+ type: POLL_VOTE_FAIL,
+ error,
+});
+
+export const fetchPollRequest = () => ({
+ type: POLL_FETCH_REQUEST,
+});
+
+export const fetchPollSuccess = poll => ({
+ type: POLL_FETCH_SUCCESS,
+ poll,
+});
+
+export const fetchPollFail = error => ({
+ type: POLL_FETCH_FAIL,
+ error,
+});
diff --git a/app/javascript/mastodon/actions/polls.ts b/app/javascript/mastodon/actions/polls.ts
deleted file mode 100644
index 65a96e8f62..0000000000
--- a/app/javascript/mastodon/actions/polls.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { apiGetPoll, apiPollVote } from 'mastodon/api/polls';
-import type { ApiPollJSON } from 'mastodon/api_types/polls';
-import { createPollFromServerJSON } from 'mastodon/models/poll';
-import {
- createAppAsyncThunk,
- createDataLoadingThunk,
-} from 'mastodon/store/typed_functions';
-
-import { importPolls } from './importer/polls';
-
-export const importFetchedPoll = createAppAsyncThunk(
- 'poll/importFetched',
- (args: { poll: ApiPollJSON }, { dispatch, getState }) => {
- const { poll } = args;
-
- dispatch(
- importPolls({
- polls: [createPollFromServerJSON(poll, getState().polls[poll.id])],
- }),
- );
- },
-);
-
-export const vote = createDataLoadingThunk(
- 'poll/vote',
- ({ pollId, choices }: { pollId: string; choices: string[] }) =>
- apiPollVote(pollId, choices),
- async (poll, { dispatch, discardLoadData }) => {
- await dispatch(importFetchedPoll({ poll }));
- return discardLoadData;
- },
-);
-
-export const fetchPoll = createDataLoadingThunk(
- 'poll/fetch',
- ({ pollId }: { pollId: string }) => apiGetPoll(pollId),
- async (poll, { dispatch }) => {
- await dispatch(importFetchedPoll({ poll }));
- },
-);
diff --git a/app/javascript/mastodon/actions/push_notifications/registerer.js b/app/javascript/mastodon/actions/push_notifications/registerer.js
index 647a6bd9fb..b3d3850e31 100644
--- a/app/javascript/mastodon/actions/push_notifications/registerer.js
+++ b/app/javascript/mastodon/actions/push_notifications/registerer.js
@@ -33,7 +33,7 @@ const unsubscribe = ({ registration, subscription }) =>
subscription ? subscription.unsubscribe().then(() => registration) : registration;
const sendSubscriptionToBackend = (subscription) => {
- const params = { subscription: { ...subscription.toJSON(), standard: true } };
+ const params = { subscription };
if (me) {
const data = pushNotificationsSetting.get(me);
diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js
new file mode 100644
index 0000000000..bde17ae0db
--- /dev/null
+++ b/app/javascript/mastodon/actions/search.js
@@ -0,0 +1,215 @@
+import { fromJS } from 'immutable';
+
+import { searchHistory } from 'mastodon/settings';
+
+import api from '../api';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts, importFetchedStatuses } from './importer';
+
+export const SEARCH_CHANGE = 'SEARCH_CHANGE';
+export const SEARCH_CLEAR = 'SEARCH_CLEAR';
+export const SEARCH_SHOW = 'SEARCH_SHOW';
+
+export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST';
+export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS';
+export const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL';
+
+export const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST';
+export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS';
+export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL';
+
+export const SEARCH_HISTORY_UPDATE = 'SEARCH_HISTORY_UPDATE';
+
+export function changeSearch(value) {
+ return {
+ type: SEARCH_CHANGE,
+ value,
+ };
+}
+
+export function clearSearch() {
+ return {
+ type: SEARCH_CLEAR,
+ };
+}
+
+export function submitSearch(type) {
+ return (dispatch, getState) => {
+ const value = getState().getIn(['search', 'value']);
+ const signedIn = !!getState().getIn(['meta', 'me']);
+
+ if (value.length === 0) {
+ dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, '', type));
+ return;
+ }
+
+ dispatch(fetchSearchRequest(type));
+
+ api().get('/api/v2/search', {
+ params: {
+ q: value,
+ resolve: signedIn,
+ limit: 11,
+ type,
+ },
+ }).then(response => {
+ if (response.data.accounts) {
+ dispatch(importFetchedAccounts(response.data.accounts));
+ }
+
+ if (response.data.statuses) {
+ dispatch(importFetchedStatuses(response.data.statuses));
+ }
+
+ dispatch(fetchSearchSuccess(response.data, value, type));
+ dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
+ }).catch(error => {
+ dispatch(fetchSearchFail(error));
+ });
+ };
+}
+
+export function fetchSearchRequest(searchType) {
+ return {
+ type: SEARCH_FETCH_REQUEST,
+ searchType,
+ };
+}
+
+export function fetchSearchSuccess(results, searchTerm, searchType) {
+ return {
+ type: SEARCH_FETCH_SUCCESS,
+ results,
+ searchType,
+ searchTerm,
+ };
+}
+
+export function fetchSearchFail(error) {
+ return {
+ type: SEARCH_FETCH_FAIL,
+ error,
+ };
+}
+
+export const expandSearch = type => (dispatch, getState) => {
+ const value = getState().getIn(['search', 'value']);
+ const offset = getState().getIn(['search', 'results', type]).size - 1;
+
+ dispatch(expandSearchRequest(type));
+
+ api().get('/api/v2/search', {
+ params: {
+ q: value,
+ type,
+ offset,
+ limit: 11,
+ },
+ }).then(({ data }) => {
+ if (data.accounts) {
+ dispatch(importFetchedAccounts(data.accounts));
+ }
+
+ if (data.statuses) {
+ dispatch(importFetchedStatuses(data.statuses));
+ }
+
+ dispatch(expandSearchSuccess(data, value, type));
+ dispatch(fetchRelationships(data.accounts.map(item => item.id)));
+ }).catch(error => {
+ dispatch(expandSearchFail(error));
+ });
+};
+
+export const expandSearchRequest = (searchType) => ({
+ type: SEARCH_EXPAND_REQUEST,
+ searchType,
+});
+
+export const expandSearchSuccess = (results, searchTerm, searchType) => ({
+ type: SEARCH_EXPAND_SUCCESS,
+ results,
+ searchTerm,
+ searchType,
+});
+
+export const expandSearchFail = error => ({
+ type: SEARCH_EXPAND_FAIL,
+ error,
+});
+
+export const showSearch = () => ({
+ type: SEARCH_SHOW,
+});
+
+export const openURL = (value, history, onFailure) => (dispatch, getState) => {
+ const signedIn = !!getState().getIn(['meta', 'me']);
+
+ if (!signedIn) {
+ if (onFailure) {
+ onFailure();
+ }
+
+ return;
+ }
+
+ dispatch(fetchSearchRequest());
+
+ api().get('/api/v2/search', { params: { q: value, resolve: true } }).then(response => {
+ if (response.data.accounts?.length > 0) {
+ dispatch(importFetchedAccounts(response.data.accounts));
+ history.push(`/@${response.data.accounts[0].acct}`);
+ } else if (response.data.statuses?.length > 0) {
+ dispatch(importFetchedStatuses(response.data.statuses));
+ history.push(`/@${response.data.statuses[0].account.acct}/${response.data.statuses[0].id}`);
+ } else if (onFailure) {
+ onFailure();
+ }
+
+ dispatch(fetchSearchSuccess(response.data, value));
+ }).catch(err => {
+ dispatch(fetchSearchFail(err));
+
+ if (onFailure) {
+ onFailure();
+ }
+ });
+};
+
+export const clickSearchResult = (q, type) => (dispatch, getState) => {
+ const previous = getState().getIn(['search', 'recent']);
+
+ if (previous.some(x => x.get('q') === q && x.get('type') === type)) {
+ return;
+ }
+
+ const me = getState().getIn(['meta', 'me']);
+ const current = previous.add(fromJS({ type, q })).takeLast(4);
+
+ searchHistory.set(me, current.toJS());
+ dispatch(updateSearchHistory(current));
+};
+
+export const forgetSearchResult = q => (dispatch, getState) => {
+ const previous = getState().getIn(['search', 'recent']);
+ const me = getState().getIn(['meta', 'me']);
+ const current = previous.filterNot(result => result.get('q') === q);
+
+ searchHistory.set(me, current.toJS());
+ dispatch(updateSearchHistory(current));
+};
+
+export const updateSearchHistory = recent => ({
+ type: SEARCH_HISTORY_UPDATE,
+ recent,
+});
+
+export const hydrateSearch = () => (dispatch, getState) => {
+ const me = getState().getIn(['meta', 'me']);
+ const history = searchHistory.get(me);
+
+ if (history !== null) {
+ dispatch(updateSearchHistory(history));
+ }
+};
diff --git a/app/javascript/mastodon/actions/search.ts b/app/javascript/mastodon/actions/search.ts
deleted file mode 100644
index 13a4ee4432..0000000000
--- a/app/javascript/mastodon/actions/search.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import { createAction } from '@reduxjs/toolkit';
-
-import { apiGetSearch } from 'mastodon/api/search';
-import type { ApiSearchType } from 'mastodon/api_types/search';
-import type {
- RecentSearch,
- SearchType as RecentSearchType,
-} from 'mastodon/models/search';
-import { searchHistory } from 'mastodon/settings';
-import {
- createDataLoadingThunk,
- createAppAsyncThunk,
-} from 'mastodon/store/typed_functions';
-
-import { fetchRelationships } from './accounts';
-import { importFetchedAccounts, importFetchedStatuses } from './importer';
-
-export const SEARCH_HISTORY_UPDATE = 'SEARCH_HISTORY_UPDATE';
-
-export const submitSearch = createDataLoadingThunk(
- 'search/submit',
- async ({ q, type }: { q: string; type?: ApiSearchType }, { getState }) => {
- const signedIn = !!getState().meta.get('me');
-
- return apiGetSearch({
- q,
- type,
- resolve: signedIn,
- limit: 11,
- });
- },
- (data, { dispatch }) => {
- if (data.accounts.length > 0) {
- dispatch(importFetchedAccounts(data.accounts));
- dispatch(fetchRelationships(data.accounts.map((account) => account.id)));
- }
-
- if (data.statuses.length > 0) {
- dispatch(importFetchedStatuses(data.statuses));
- }
-
- return data;
- },
- {
- useLoadingBar: false,
- },
-);
-
-export const expandSearch = createDataLoadingThunk(
- 'search/expand',
- async ({ type }: { type: ApiSearchType }, { getState }) => {
- const q = getState().search.q;
- const results = getState().search.results;
- const offset = results?.[type].length;
-
- return apiGetSearch({
- q,
- type,
- limit: 10,
- offset,
- });
- },
- (data, { dispatch }) => {
- if (data.accounts.length > 0) {
- dispatch(importFetchedAccounts(data.accounts));
- dispatch(fetchRelationships(data.accounts.map((account) => account.id)));
- }
-
- if (data.statuses.length > 0) {
- dispatch(importFetchedStatuses(data.statuses));
- }
-
- return data;
- },
- {
- useLoadingBar: true,
- },
-);
-
-export const openURL = createDataLoadingThunk(
- 'search/openURL',
- ({ url }: { url: string }) =>
- apiGetSearch({
- q: url,
- resolve: true,
- limit: 1,
- }),
- (data, { dispatch }) => {
- if (data.accounts.length > 0) {
- dispatch(importFetchedAccounts(data.accounts));
- } else if (data.statuses.length > 0) {
- dispatch(importFetchedStatuses(data.statuses));
- }
-
- return data;
- },
- {
- useLoadingBar: true,
- },
-);
-
-export const clickSearchResult = createAppAsyncThunk(
- 'search/clickResult',
- (
- { q, type }: { q: string; type?: RecentSearchType },
- { dispatch, getState },
- ) => {
- const previous = getState().search.recent;
-
- if (previous.some((x) => x.q === q && x.type === type)) {
- return;
- }
-
- const me = getState().meta.get('me') as string;
- const current = [{ type, q }, ...previous].slice(0, 4);
-
- searchHistory.set(me, current);
- dispatch(updateSearchHistory(current));
- },
-);
-
-export const forgetSearchResult = createAppAsyncThunk(
- 'search/forgetResult',
- (q: string, { dispatch, getState }) => {
- const previous = getState().search.recent;
- const me = getState().meta.get('me') as string;
- const current = previous.filter((result) => result.q !== q);
-
- searchHistory.set(me, current);
- dispatch(updateSearchHistory(current));
- },
-);
-
-export const updateSearchHistory = createAction(
- 'search/updateHistory',
-);
-
-export const hydrateSearch = createAppAsyncThunk(
- 'search/hydrate',
- (_args, { dispatch, getState }) => {
- const me = getState().meta.get('me') as string;
- const history = searchHistory.get(me) as RecentSearch[] | null;
-
- if (history !== null) {
- dispatch(updateSearchHistory(history));
- }
- },
-);
diff --git a/app/javascript/mastodon/actions/settings.js b/app/javascript/mastodon/actions/settings.js
index 7659fb5f98..fbd89f9d4b 100644
--- a/app/javascript/mastodon/actions/settings.js
+++ b/app/javascript/mastodon/actions/settings.js
@@ -29,7 +29,7 @@ const debouncedSave = debounce((dispatch, getState) => {
api().put('/api/web/settings', { data })
.then(() => dispatch({ type: SETTING_SAVE }))
.catch(error => dispatch(showAlertForError(error)));
-}, 2000, { leading: true, trailing: true });
+}, 5000, { trailing: true });
export function saveSettings() {
return (dispatch, getState) => debouncedSave(dispatch, getState);
diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js
index 5064e65e7b..40ead34782 100644
--- a/app/javascript/mastodon/actions/statuses.js
+++ b/app/javascript/mastodon/actions/statuses.js
@@ -148,7 +148,7 @@ export function deleteStatus(id, withRedraft = false) {
dispatch(deleteStatusRequest(id));
- api().delete(`/api/v1/statuses/${id}`, { params: { delete_media: !withRedraft } }).then(response => {
+ api().delete(`/api/v1/statuses/${id}`).then(response => {
dispatch(deleteStatusSuccess(id));
dispatch(deleteFromTimelines(id));
dispatch(importFetchedAccount(response.data.account));
diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js
index e8fec13453..8ab75cdc44 100644
--- a/app/javascript/mastodon/actions/store.js
+++ b/app/javascript/mastodon/actions/store.js
@@ -1,4 +1,4 @@
-import { fromJS, isIndexed } from 'immutable';
+import { Iterable, fromJS } from 'immutable';
import { hydrateCompose } from './compose';
import { importFetchedAccounts } from './importer';
@@ -9,7 +9,8 @@ export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
const convertState = rawState =>
fromJS(rawState, (k, v) =>
- isIndexed(v) ? v.toList() : v.toMap());
+ Iterable.isIndexed(v) ? v.toList() : v.toMap());
+
export function hydrateStore(rawState) {
return dispatch => {
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index f9d784c2b4..a828900ec9 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -11,7 +11,7 @@ import {
} from './announcements';
import { updateConversations } from './conversations';
import { processNewNotificationForGroups, refreshStaleNotificationGroups, pollRecentNotifications as pollRecentGroupNotifications } from './notification_groups';
-import { updateNotifications, updateEmojiReactions } from './notifications';
+import { updateNotifications, expandNotifications, updateEmojiReactions } from './notifications';
import { updateStatus } from './statuses';
import {
updateTimeline,
@@ -111,10 +111,12 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
// @ts-expect-error
dispatch(updateEmojiReactions(JSON.parse(data.payload)));
break;
- case 'notifications_merged': {
+ case 'notifications_merged':
+ const state = getState();
+ if (state.notifications.top || !state.notifications.mounted)
+ dispatch(expandNotifications({ forceLoad: true, maxId: undefined }));
dispatch(refreshStaleNotificationGroups());
break;
- }
case 'conversation':
// @ts-expect-error
dispatch(updateConversations(JSON.parse(data.payload)));
diff --git a/app/javascript/mastodon/actions/suggestions.js b/app/javascript/mastodon/actions/suggestions.js
new file mode 100644
index 0000000000..258ffa901d
--- /dev/null
+++ b/app/javascript/mastodon/actions/suggestions.js
@@ -0,0 +1,58 @@
+import api from '../api';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
+
+export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST';
+export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS';
+export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL';
+
+export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS';
+
+export function fetchSuggestions(withRelationships = false) {
+ return (dispatch) => {
+ dispatch(fetchSuggestionsRequest());
+
+ api().get('/api/v2/suggestions', { params: { limit: 20 } }).then(response => {
+ dispatch(importFetchedAccounts(response.data.map(x => x.account)));
+ dispatch(fetchSuggestionsSuccess(response.data));
+
+ if (withRelationships) {
+ dispatch(fetchRelationships(response.data.map(item => item.account.id)));
+ }
+ }).catch(error => dispatch(fetchSuggestionsFail(error)));
+ };
+}
+
+export function fetchSuggestionsRequest() {
+ return {
+ type: SUGGESTIONS_FETCH_REQUEST,
+ skipLoading: true,
+ };
+}
+
+export function fetchSuggestionsSuccess(suggestions) {
+ return {
+ type: SUGGESTIONS_FETCH_SUCCESS,
+ suggestions,
+ skipLoading: true,
+ };
+}
+
+export function fetchSuggestionsFail(error) {
+ return {
+ type: SUGGESTIONS_FETCH_FAIL,
+ error,
+ skipLoading: true,
+ skipAlert: true,
+ };
+}
+
+export const dismissSuggestion = accountId => (dispatch) => {
+ dispatch({
+ type: SUGGESTIONS_DISMISS,
+ id: accountId,
+ });
+
+ api().delete(`/api/v1/suggestions/${accountId}`).catch(() => {});
+};
diff --git a/app/javascript/mastodon/actions/suggestions.ts b/app/javascript/mastodon/actions/suggestions.ts
deleted file mode 100644
index 0eadfa6b47..0000000000
--- a/app/javascript/mastodon/actions/suggestions.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import {
- apiGetSuggestions,
- apiDeleteSuggestion,
-} from 'mastodon/api/suggestions';
-import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
-
-import { fetchRelationships } from './accounts';
-import { importFetchedAccounts } from './importer';
-
-export const fetchSuggestions = createDataLoadingThunk(
- 'suggestions/fetch',
- () => apiGetSuggestions(20),
- (data, { dispatch }) => {
- dispatch(importFetchedAccounts(data.map((x) => x.account)));
- dispatch(fetchRelationships(data.map((x) => x.account.id)));
-
- return data;
- },
-);
-
-export const dismissSuggestion = createDataLoadingThunk(
- 'suggestions/dismiss',
- ({ accountId }: { accountId: string }) => apiDeleteSuggestion(accountId),
-);
diff --git a/app/javascript/mastodon/actions/tags.js b/app/javascript/mastodon/actions/tags.js
new file mode 100644
index 0000000000..d18d7e514f
--- /dev/null
+++ b/app/javascript/mastodon/actions/tags.js
@@ -0,0 +1,172 @@
+import api, { getLinks } from '../api';
+
+export const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST';
+export const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS';
+export const HASHTAG_FETCH_FAIL = 'HASHTAG_FETCH_FAIL';
+
+export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST';
+export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS';
+export const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL';
+
+export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST';
+export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS';
+export const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL';
+
+export const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST';
+export const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS';
+export const HASHTAG_FOLLOW_FAIL = 'HASHTAG_FOLLOW_FAIL';
+
+export const HASHTAG_UNFOLLOW_REQUEST = 'HASHTAG_UNFOLLOW_REQUEST';
+export const HASHTAG_UNFOLLOW_SUCCESS = 'HASHTAG_UNFOLLOW_SUCCESS';
+export const HASHTAG_UNFOLLOW_FAIL = 'HASHTAG_UNFOLLOW_FAIL';
+
+export const fetchHashtag = name => (dispatch) => {
+ dispatch(fetchHashtagRequest());
+
+ api().get(`/api/v1/tags/${name}`).then(({ data }) => {
+ dispatch(fetchHashtagSuccess(name, data));
+ }).catch(err => {
+ dispatch(fetchHashtagFail(err));
+ });
+};
+
+export const fetchHashtagRequest = () => ({
+ type: HASHTAG_FETCH_REQUEST,
+});
+
+export const fetchHashtagSuccess = (name, tag) => ({
+ type: HASHTAG_FETCH_SUCCESS,
+ name,
+ tag,
+});
+
+export const fetchHashtagFail = error => ({
+ type: HASHTAG_FETCH_FAIL,
+ error,
+});
+
+export const fetchFollowedHashtags = () => (dispatch) => {
+ dispatch(fetchFollowedHashtagsRequest());
+
+ api().get('/api/v1/followed_tags').then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null));
+ }).catch(err => {
+ dispatch(fetchFollowedHashtagsFail(err));
+ });
+};
+
+export function fetchFollowedHashtagsRequest() {
+ return {
+ type: FOLLOWED_HASHTAGS_FETCH_REQUEST,
+ };
+}
+
+export function fetchFollowedHashtagsSuccess(followed_tags, next) {
+ return {
+ type: FOLLOWED_HASHTAGS_FETCH_SUCCESS,
+ followed_tags,
+ next,
+ };
+}
+
+export function fetchFollowedHashtagsFail(error) {
+ return {
+ type: FOLLOWED_HASHTAGS_FETCH_FAIL,
+ error,
+ };
+}
+
+export function expandFollowedHashtags() {
+ return (dispatch, getState) => {
+ const url = getState().getIn(['followed_tags', 'next']);
+
+ if (url === null) {
+ return;
+ }
+
+ dispatch(expandFollowedHashtagsRequest());
+
+ api().get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null));
+ }).catch(error => {
+ dispatch(expandFollowedHashtagsFail(error));
+ });
+ };
+}
+
+export function expandFollowedHashtagsRequest() {
+ return {
+ type: FOLLOWED_HASHTAGS_EXPAND_REQUEST,
+ };
+}
+
+export function expandFollowedHashtagsSuccess(followed_tags, next) {
+ return {
+ type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
+ followed_tags,
+ next,
+ };
+}
+
+export function expandFollowedHashtagsFail(error) {
+ return {
+ type: FOLLOWED_HASHTAGS_EXPAND_FAIL,
+ error,
+ };
+}
+
+export const followHashtag = name => (dispatch) => {
+ dispatch(followHashtagRequest(name));
+
+ api().post(`/api/v1/tags/${name}/follow`).then(({ data }) => {
+ dispatch(followHashtagSuccess(name, data));
+ }).catch(err => {
+ dispatch(followHashtagFail(name, err));
+ });
+};
+
+export const followHashtagRequest = name => ({
+ type: HASHTAG_FOLLOW_REQUEST,
+ name,
+});
+
+export const followHashtagSuccess = (name, tag) => ({
+ type: HASHTAG_FOLLOW_SUCCESS,
+ name,
+ tag,
+});
+
+export const followHashtagFail = (name, error) => ({
+ type: HASHTAG_FOLLOW_FAIL,
+ name,
+ error,
+});
+
+export const unfollowHashtag = name => (dispatch) => {
+ dispatch(unfollowHashtagRequest(name));
+
+ api().post(`/api/v1/tags/${name}/unfollow`).then(({ data }) => {
+ dispatch(unfollowHashtagSuccess(name, data));
+ }).catch(err => {
+ dispatch(unfollowHashtagFail(name, err));
+ });
+};
+
+export const unfollowHashtagRequest = name => ({
+ type: HASHTAG_UNFOLLOW_REQUEST,
+ name,
+});
+
+export const unfollowHashtagSuccess = (name, tag) => ({
+ type: HASHTAG_UNFOLLOW_SUCCESS,
+ name,
+ tag,
+});
+
+export const unfollowHashtagFail = (name, error) => ({
+ type: HASHTAG_UNFOLLOW_FAIL,
+ name,
+ error,
+});
diff --git a/app/javascript/mastodon/actions/tags_typed.ts b/app/javascript/mastodon/actions/tags_typed.ts
deleted file mode 100644
index 6dca32fd84..0000000000
--- a/app/javascript/mastodon/actions/tags_typed.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { apiGetTag, apiFollowTag, apiUnfollowTag } from 'mastodon/api/tags';
-import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
-
-export const fetchHashtag = createDataLoadingThunk(
- 'tags/fetch',
- ({ tagId }: { tagId: string }) => apiGetTag(tagId),
-);
-
-export const followHashtag = createDataLoadingThunk(
- 'tags/follow',
- ({ tagId }: { tagId: string }) => apiFollowTag(tagId),
-);
-
-export const unfollowHashtag = createDataLoadingThunk(
- 'tags/unfollow',
- ({ tagId }: { tagId: string }) => apiUnfollowTag(tagId),
-);
diff --git a/app/javascript/mastodon/api.ts b/app/javascript/mastodon/api.ts
index a41b058d2c..51cbe0b695 100644
--- a/app/javascript/mastodon/api.ts
+++ b/app/javascript/mastodon/api.ts
@@ -1,9 +1,4 @@
-import type {
- AxiosError,
- AxiosResponse,
- Method,
- RawAxiosRequestHeaders,
-} from 'axios';
+import type { AxiosResponse, Method, RawAxiosRequestHeaders } from 'axios';
import axios from 'axios';
import LinkHeader from 'http-link-header';
@@ -46,7 +41,7 @@ const authorizationTokenFromInitialState = (): RawAxiosRequestHeaders => {
// eslint-disable-next-line import/no-default-export
export default function api(withAuthorization = true) {
- const instance = axios.create({
+ return axios.create({
transitional: {
clarifyTimeoutError: true,
},
@@ -65,22 +60,6 @@ export default function api(withAuthorization = true) {
},
],
});
-
- instance.interceptors.response.use(
- (response: AxiosResponse) => {
- if (response.headers.deprecation) {
- console.warn(
- `Deprecated request: ${response.config.method} ${response.config.url}`,
- );
- }
- return response;
- },
- (error: AxiosError) => {
- return Promise.reject(error);
- },
- );
-
- return instance;
}
type RequestParamsOrData = Record;
@@ -89,7 +68,6 @@ export async function apiRequest(
method: Method,
url: string,
args: {
- signal?: AbortSignal;
params?: RequestParamsOrData;
data?: RequestParamsOrData;
timeout?: number;
diff --git a/app/javascript/mastodon/api/accounts.ts b/app/javascript/mastodon/api/accounts.ts
index 717010ba74..bd1757e827 100644
--- a/app/javascript/mastodon/api/accounts.ts
+++ b/app/javascript/mastodon/api/accounts.ts
@@ -5,16 +5,3 @@ export const apiSubmitAccountNote = (id: string, value: string) =>
apiRequestPost(`v1/accounts/${id}/note`, {
comment: value,
});
-
-export const apiFollowAccount = (
- id: string,
- params?: {
- reblogs: boolean;
- },
-) =>
- apiRequestPost(`v1/accounts/${id}/follow`, {
- ...params,
- });
-
-export const apiUnfollowAccount = (id: string) =>
- apiRequestPost(`v1/accounts/${id}/unfollow`);
diff --git a/app/javascript/mastodon/api/antennas.ts b/app/javascript/mastodon/api/antennas.ts
deleted file mode 100644
index 61fd84185d..0000000000
--- a/app/javascript/mastodon/api/antennas.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-import {
- apiRequestPost,
- apiRequestPut,
- apiRequestGet,
- apiRequestDelete,
-} from 'mastodon/api';
-import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
-import type { ApiAntennaJSON } from 'mastodon/api_types/antennas';
-
-export const apiCreate = (antenna: Partial) =>
- apiRequestPost('v1/antennas', antenna);
-
-export const apiUpdate = (antenna: Partial) =>
- apiRequestPut(`v1/antennas/${antenna.id}`, antenna);
-
-export const apiGetAccounts = (antennaId: string) =>
- apiRequestGet(`v1/antennas/${antennaId}/accounts`, {
- limit: 0,
- });
-
-export const apiGetExcludeAccounts = (antennaId: string) =>
- apiRequestGet(`v1/antennas/${antennaId}/exclude_accounts`, {
- limit: 0,
- });
-
-export const apiGetDomains = (antennaId: string) =>
- apiRequestGet<{ domains: string[]; exclude_domains: string[] }>(
- `v1/antennas/${antennaId}/domains`,
- {
- limit: 0,
- },
- );
-
-export const apiAddDomain = (antennaId: string, domain: string) =>
- apiRequestPost(`v1/antennas/${antennaId}/domains`, {
- domains: [domain],
- });
-
-export const apiRemoveDomain = (antennaId: string, domain: string) =>
- apiRequestDelete(`v1/antennas/${antennaId}/domains`, {
- domains: [domain],
- });
-
-export const apiAddExcludeDomain = (antennaId: string, domain: string) =>
- apiRequestPost(`v1/antennas/${antennaId}/exclude_domains`, {
- domains: [domain],
- });
-
-export const apiRemoveExcludeDomain = (antennaId: string, domain: string) =>
- apiRequestDelete(`v1/antennas/${antennaId}/exclude_domains`, {
- domains: [domain],
- });
-
-export const apiGetTags = (antennaId: string) =>
- apiRequestGet<{ tags: string[]; exclude_tags: string[] }>(
- `v1/antennas/${antennaId}/tags`,
- {
- limit: 0,
- },
- );
-
-export const apiAddTag = (antennaId: string, tag: string) =>
- apiRequestPost(`v1/antennas/${antennaId}/tags`, {
- tags: [tag],
- });
-
-export const apiRemoveTag = (antennaId: string, tag: string) =>
- apiRequestDelete(`v1/antennas/${antennaId}/tags`, {
- tags: [tag],
- });
-
-export const apiAddExcludeTag = (antennaId: string, tag: string) =>
- apiRequestPost(`v1/antennas/${antennaId}/exclude_tags`, {
- tags: [tag],
- });
-
-export const apiRemoveExcludeTag = (antennaId: string, tag: string) =>
- apiRequestDelete(`v1/antennas/${antennaId}/exclude_tags`, {
- tags: [tag],
- });
-
-export const apiGetKeywords = (antennaId: string) =>
- apiRequestGet<{ keywords: string[]; exclude_keywords: string[] }>(
- `v1/antennas/${antennaId}/keywords`,
- {
- limit: 0,
- },
- );
-
-export const apiAddKeyword = (antennaId: string, keyword: string) =>
- apiRequestPost(`v1/antennas/${antennaId}/keywords`, {
- keywords: [keyword],
- });
-
-export const apiRemoveKeyword = (antennaId: string, keyword: string) =>
- apiRequestDelete(`v1/antennas/${antennaId}/keywords`, {
- keywords: [keyword],
- });
-
-export const apiAddExcludeKeyword = (antennaId: string, keyword: string) =>
- apiRequestPost(`v1/antennas/${antennaId}/exclude_keywords`, {
- keywords: [keyword],
- });
-
-export const apiRemoveExcludeKeyword = (antennaId: string, keyword: string) =>
- apiRequestDelete(`v1/antennas/${antennaId}/exclude_keywords`, {
- keywords: [keyword],
- });
-
-export const apiGetAccountAntennas = (accountId: string) =>
- apiRequestGet(`v1/accounts/${accountId}/antennas`);
-
-export const apiAddAccountToAntenna = (antennaId: string, accountId: string) =>
- apiRequestPost(`v1/antennas/${antennaId}/accounts`, {
- account_ids: [accountId],
- });
-
-export const apiRemoveAccountFromAntenna = (
- antennaId: string,
- accountId: string,
-) =>
- apiRequestDelete(`v1/antennas/${antennaId}/accounts`, {
- account_ids: [accountId],
- });
-
-export const apiGetExcludeAccountAntennas = (accountId: string) =>
- apiRequestGet(`v1/accounts/${accountId}/exclude_antennas`);
-
-export const apiAddExcludeAccountToAntenna = (
- antennaId: string,
- accountId: string,
-) =>
- apiRequestPost(`v1/antennas/${antennaId}/exclude_accounts`, {
- account_ids: [accountId],
- });
-
-export const apiRemoveExcludeAccountFromAntenna = (
- antennaId: string,
- accountId: string,
-) =>
- apiRequestDelete(`v1/antennas/${antennaId}/exclude_accounts`, {
- account_ids: [accountId],
- });
diff --git a/app/javascript/mastodon/api/bookmark_categories.ts b/app/javascript/mastodon/api/bookmark_categories.ts
deleted file mode 100644
index d6d3394b3a..0000000000
--- a/app/javascript/mastodon/api/bookmark_categories.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import {
- apiRequestPost,
- apiRequestPut,
- apiRequestGet,
- apiRequestDelete,
-} from 'mastodon/api';
-import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
-import type { ApiBookmarkCategoryJSON } from 'mastodon/api_types/bookmark_categories';
-
-export const apiCreate = (bookmarkCategory: Partial) =>
- apiRequestPost(
- 'v1/bookmark_categories',
- bookmarkCategory,
- );
-
-export const apiUpdate = (bookmarkCategory: Partial) =>
- apiRequestPut(
- `v1/bookmark_categories/${bookmarkCategory.id}`,
- bookmarkCategory,
- );
-
-export const apiGetStatuses = (bookmarkCategoryId: string) =>
- apiRequestGet(
- `v1/bookmark_categories/${bookmarkCategoryId}/statuses`,
- {
- limit: 0,
- },
- );
-
-export const apiGetStatusBookmarkCategories = (accountId: string) =>
- apiRequestGet(
- `v1/statuses/${accountId}/bookmark_categories`,
- );
-
-export const apiAddStatusToBookmarkCategory = (
- bookmarkCategoryId: string,
- statusId: string,
-) =>
- apiRequestPost(`v1/bookmark_categories/${bookmarkCategoryId}/statuses`, {
- status_ids: [statusId],
- });
-
-export const apiRemoveStatusFromBookmarkCategory = (
- bookmarkCategoryId: string,
- statusId: string,
-) =>
- apiRequestDelete(`v1/bookmark_categories/${bookmarkCategoryId}/statuses`, {
- status_ids: [statusId],
- });
diff --git a/app/javascript/mastodon/api/circles.ts b/app/javascript/mastodon/api/circles.ts
deleted file mode 100644
index 04971e1e6b..0000000000
--- a/app/javascript/mastodon/api/circles.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import {
- apiRequestPost,
- apiRequestPut,
- apiRequestGet,
- apiRequestDelete,
-} from 'mastodon/api';
-import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
-import type { ApiCircleJSON } from 'mastodon/api_types/circles';
-
-export const apiCreate = (circle: Partial) =>
- apiRequestPost('v1/circles', circle);
-
-export const apiUpdate = (circle: Partial) =>
- apiRequestPut(`v1/circles/${circle.id}`, circle);
-
-export const apiGetAccounts = (circleId: string) =>
- apiRequestGet(`v1/circles/${circleId}/accounts`, {
- limit: 0,
- });
-
-export const apiGetAccountCircles = (accountId: string) =>
- apiRequestGet(`v1/accounts/${accountId}/circles`);
-
-export const apiAddAccountToCircle = (circleId: string, accountId: string) =>
- apiRequestPost(`v1/circles/${circleId}/accounts`, {
- account_ids: [accountId],
- });
-
-export const apiRemoveAccountFromCircle = (
- circleId: string,
- accountId: string,
-) =>
- apiRequestDelete(`v1/circles/${circleId}/accounts`, {
- account_ids: [accountId],
- });
diff --git a/app/javascript/mastodon/api/compose.ts b/app/javascript/mastodon/api/compose.ts
deleted file mode 100644
index 757e9961c9..0000000000
--- a/app/javascript/mastodon/api/compose.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { apiRequestPut } from 'mastodon/api';
-import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments';
-
-export const apiUpdateMedia = (
- id: string,
- params?: { description?: string; focus?: string },
-) => apiRequestPut(`v1/media/${id}`, params);
diff --git a/app/javascript/mastodon/api/domain_blocks.ts b/app/javascript/mastodon/api/domain_blocks.ts
deleted file mode 100644
index 4e153b0ee9..0000000000
--- a/app/javascript/mastodon/api/domain_blocks.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import api, { getLinks } from 'mastodon/api';
-
-export const apiGetDomainBlocks = async (url?: string) => {
- const response = await api().request({
- method: 'GET',
- url: url ?? '/api/v1/domain_blocks',
- });
-
- return {
- domains: response.data,
- links: getLinks(response),
- };
-};
diff --git a/app/javascript/mastodon/api/instance.ts b/app/javascript/mastodon/api/instance.ts
deleted file mode 100644
index 764e8daab2..0000000000
--- a/app/javascript/mastodon/api/instance.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { apiRequestGet } from 'mastodon/api';
-import type {
- ApiTermsOfServiceJSON,
- ApiPrivacyPolicyJSON,
-} from 'mastodon/api_types/instance';
-
-export const apiGetTermsOfService = (version?: string) =>
- apiRequestGet(
- version
- ? `v1/instance/terms_of_service/${version}`
- : 'v1/instance/terms_of_service',
- );
-
-export const apiGetPrivacyPolicy = () =>
- apiRequestGet('v1/instance/privacy_policy');
diff --git a/app/javascript/mastodon/api/lists.ts b/app/javascript/mastodon/api/lists.ts
deleted file mode 100644
index a5586eb6d4..0000000000
--- a/app/javascript/mastodon/api/lists.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import {
- apiRequestPost,
- apiRequestPut,
- apiRequestGet,
- apiRequestDelete,
-} from 'mastodon/api';
-import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
-import type { ApiListJSON } from 'mastodon/api_types/lists';
-
-export const apiCreate = (list: Partial) =>
- apiRequestPost('v1/lists', list);
-
-export const apiUpdate = (list: Partial) =>
- apiRequestPut(`v1/lists/${list.id}`, list);
-
-export const apiGetAccounts = (listId: string) =>
- apiRequestGet(`v1/lists/${listId}/accounts`, {
- limit: 0,
- });
-
-export const apiGetAccountLists = (accountId: string) =>
- apiRequestGet(`v1/accounts/${accountId}/lists`);
-
-export const apiAddAccountToList = (listId: string, accountId: string) =>
- apiRequestPost(`v1/lists/${listId}/accounts`, {
- account_ids: [accountId],
- });
-
-export const apiRemoveAccountFromList = (listId: string, accountId: string) =>
- apiRequestDelete(`v1/lists/${listId}/accounts`, {
- account_ids: [accountId],
- });
diff --git a/app/javascript/mastodon/api/polls.ts b/app/javascript/mastodon/api/polls.ts
deleted file mode 100644
index cb659986f5..0000000000
--- a/app/javascript/mastodon/api/polls.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { apiRequestGet, apiRequestPost } from 'mastodon/api';
-import type { ApiPollJSON } from 'mastodon/api_types/polls';
-
-export const apiGetPoll = (pollId: string) =>
- apiRequestGet(`/v1/polls/${pollId}`);
-
-export const apiPollVote = (pollId: string, choices: string[]) =>
- apiRequestPost(`/v1/polls/${pollId}/votes`, {
- choices,
- });
diff --git a/app/javascript/mastodon/api/search.ts b/app/javascript/mastodon/api/search.ts
deleted file mode 100644
index 79b0385fe8..0000000000
--- a/app/javascript/mastodon/api/search.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { apiRequestGet } from 'mastodon/api';
-import type {
- ApiSearchType,
- ApiSearchResultsJSON,
-} from 'mastodon/api_types/search';
-
-export const apiGetSearch = (params: {
- q: string;
- resolve?: boolean;
- type?: ApiSearchType;
- limit?: number;
- offset?: number;
-}) =>
- apiRequestGet('v2/search', {
- ...params,
- });
diff --git a/app/javascript/mastodon/api/suggestions.ts b/app/javascript/mastodon/api/suggestions.ts
deleted file mode 100644
index d4817698cc..0000000000
--- a/app/javascript/mastodon/api/suggestions.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { apiRequestGet, apiRequestDelete } from 'mastodon/api';
-import type { ApiSuggestionJSON } from 'mastodon/api_types/suggestions';
-
-export const apiGetSuggestions = (limit: number) =>
- apiRequestGet('v2/suggestions', { limit });
-
-export const apiDeleteSuggestion = (accountId: string) =>
- apiRequestDelete(`v1/suggestions/${accountId}`);
diff --git a/app/javascript/mastodon/api/tags.ts b/app/javascript/mastodon/api/tags.ts
deleted file mode 100644
index 4b111def81..0000000000
--- a/app/javascript/mastodon/api/tags.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import api, { getLinks, apiRequestPost, apiRequestGet } from 'mastodon/api';
-import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
-
-export const apiGetTag = (tagId: string) =>
- apiRequestGet(`v1/tags/${tagId}`);
-
-export const apiFollowTag = (tagId: string) =>
- apiRequestPost(`v1/tags/${tagId}/follow`);
-
-export const apiUnfollowTag = (tagId: string) =>
- apiRequestPost(`v1/tags/${tagId}/unfollow`);
-
-export const apiGetFollowedTags = async (url?: string) => {
- const response = await api().request({
- method: 'GET',
- url: url ?? '/api/v1/followed_tags',
- });
-
- return {
- tags: response.data,
- links: getLinks(response),
- };
-};
diff --git a/app/javascript/mastodon/api_types/accounts.ts b/app/javascript/mastodon/api_types/accounts.ts
index 9d7974eda0..a9464b1a47 100644
--- a/app/javascript/mastodon/api_types/accounts.ts
+++ b/app/javascript/mastodon/api_types/accounts.ts
@@ -39,13 +39,13 @@ export interface ApiServerFeaturesJSON {
}
// See app/serializers/rest/account_serializer.rb
-export interface BaseApiAccountJSON {
+export interface ApiAccountJSON {
acct: string;
avatar: string;
avatar_static: string;
bot: boolean;
created_at: string;
- discoverable?: boolean;
+ discoverable: boolean;
indexable: boolean;
display_name: string;
emojis: ApiCustomEmojiJSON[];
@@ -74,12 +74,3 @@ export interface BaseApiAccountJSON {
memorial?: boolean;
hide_collections: boolean;
}
-
-// See app/serializers/rest/muted_account_serializer.rb
-export interface ApiMutedAccountJSON extends BaseApiAccountJSON {
- mute_expires_at?: string | null;
-}
-
-// For now, we have the same type representing both `Account` and `MutedAccount`
-// objects, but we should refactor this in the future.
-export type ApiAccountJSON = ApiMutedAccountJSON;
diff --git a/app/javascript/mastodon/api_types/antennas.ts b/app/javascript/mastodon/api_types/antennas.ts
deleted file mode 100644
index a2a8a997ba..0000000000
--- a/app/javascript/mastodon/api_types/antennas.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-// See app/serializers/rest/antenna_serializer.rb
-
-import type { ApiListJSON } from './lists';
-
-export interface ApiAntennaJSON {
- id: string;
- title: string;
- stl: boolean;
- ltl: boolean;
- insert_feeds: boolean;
- with_media_only: boolean;
- ignore_reblog: boolean;
- favourite: boolean;
- list: ApiListJSON | null;
-
- list_id: string | undefined;
-}
diff --git a/app/javascript/mastodon/api_types/bookmark_categories.ts b/app/javascript/mastodon/api_types/bookmark_categories.ts
deleted file mode 100644
index 5407b6b125..0000000000
--- a/app/javascript/mastodon/api_types/bookmark_categories.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-// See app/serializers/rest/bookmark_category_serializer.rb
-
-export interface ApiBookmarkCategoryJSON {
- id: string;
- title: string;
-}
diff --git a/app/javascript/mastodon/api_types/circles.ts b/app/javascript/mastodon/api_types/circles.ts
deleted file mode 100644
index 9905d480b8..0000000000
--- a/app/javascript/mastodon/api_types/circles.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-// See app/serializers/rest/circle_serializer.rb
-
-export interface ApiCircleJSON {
- id: string;
- title: string;
-}
diff --git a/app/javascript/mastodon/api_types/dummy_types.ts b/app/javascript/mastodon/api_types/dummy_types.ts
new file mode 100644
index 0000000000..1f8c4db6f2
--- /dev/null
+++ b/app/javascript/mastodon/api_types/dummy_types.ts
@@ -0,0 +1,11 @@
+// A similar definition will eventually be added in the main house. These definitions will replace it.
+
+export interface ApiListJSON_KmyDummy {
+ id: string;
+ title: string;
+ exclusive: boolean;
+ notify: boolean;
+
+ // replies_policy
+ // antennas
+}
diff --git a/app/javascript/mastodon/api_types/instance.ts b/app/javascript/mastodon/api_types/instance.ts
deleted file mode 100644
index 3a29684b70..0000000000
--- a/app/javascript/mastodon/api_types/instance.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export interface ApiTermsOfServiceJSON {
- effective_date: string;
- effective: boolean;
- succeeded_by: string | null;
- content: string;
-}
-
-export interface ApiPrivacyPolicyJSON {
- updated_at: string;
- content: string;
-}
diff --git a/app/javascript/mastodon/api_types/lists.ts b/app/javascript/mastodon/api_types/lists.ts
deleted file mode 100644
index bc32b33883..0000000000
--- a/app/javascript/mastodon/api_types/lists.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-// See app/serializers/rest/list_serializer.rb
-
-import type { ApiAntennaJSON } from './antennas';
-
-export type RepliesPolicyType = 'list' | 'followed' | 'none';
-
-export interface ApiListJSON {
- id: string;
- title: string;
- exclusive: boolean;
- replies_policy: RepliesPolicyType;
- notify: boolean;
- favourite: boolean;
- antennas?: ApiAntennaJSON[];
-}
diff --git a/app/javascript/mastodon/api_types/notifications.ts b/app/javascript/mastodon/api_types/notifications.ts
index 41daed25ad..89ce9ee497 100644
--- a/app/javascript/mastodon/api_types/notifications.ts
+++ b/app/javascript/mastodon/api_types/notifications.ts
@@ -3,7 +3,7 @@
import type { AccountWarningAction } from 'mastodon/models/notification_group';
import type { ApiAccountJSON } from './accounts';
-import type { ApiListJSON } from './lists';
+import type { ApiListJSON_KmyDummy } from './dummy_types';
import type { ApiReportJSON } from './reports';
import type { ApiStatusJSON } from './statuses';
@@ -24,7 +24,6 @@ export const allNotificationTypes = [
'admin.report',
'moderation_warning',
'severed_relationships',
- 'annual_report',
];
export type NotificationWithStatusType =
@@ -45,8 +44,7 @@ export type NotificationType =
| 'moderation_warning'
| 'severed_relationships'
| 'admin.sign_up'
- | 'admin.report'
- | 'annual_report';
+ | 'admin.report';
export interface NotifyEmojiReactionJSON {
name: string;
@@ -71,7 +69,7 @@ export interface BaseNotificationJSON {
group_key: string;
account: ApiAccountJSON;
emoji_reaction?: NotifyEmojiReactionJSON;
- list?: ApiListJSON;
+ list?: ApiListJSON_KmyDummy;
}
export interface BaseNotificationGroupJSON {
@@ -84,7 +82,7 @@ export interface BaseNotificationGroupJSON {
page_min_id?: string;
page_max_id?: string;
emoji_reaction_groups?: NotificationEmojiReactionGroupJSON[];
- list?: ApiListJSON;
+ list?: ApiListJSON_KmyDummy;
}
interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON {
@@ -160,15 +158,6 @@ interface AccountRelationshipSeveranceNotificationJSON
event: ApiAccountRelationshipSeveranceEventJSON;
}
-export interface ApiAnnualReportEventJSON {
- year: string;
-}
-
-interface AnnualReportNotificationGroupJSON extends BaseNotificationGroupJSON {
- type: 'annual_report';
- annual_report: ApiAnnualReportEventJSON;
-}
-
export type ApiNotificationJSON =
| SimpleNotificationJSON
| ReportNotificationJSON
@@ -181,8 +170,7 @@ export type ApiNotificationGroupJSON =
| ReportNotificationGroupJSON
| AccountRelationshipSeveranceNotificationGroupJSON
| NotificationGroupWithStatusJSON
- | ModerationWarningNotificationGroupJSON
- | AnnualReportNotificationGroupJSON;
+ | ModerationWarningNotificationGroupJSON;
export interface ApiNotificationGroupsResultJSON {
accounts: ApiAccountJSON[];
diff --git a/app/javascript/mastodon/api_types/polls.ts b/app/javascript/mastodon/api_types/polls.ts
index 891a2faba7..8181f7b813 100644
--- a/app/javascript/mastodon/api_types/polls.ts
+++ b/app/javascript/mastodon/api_types/polls.ts
@@ -13,11 +13,11 @@ export interface ApiPollJSON {
expired: boolean;
multiple: boolean;
votes_count: number;
- voters_count: number | null;
+ voters_count: number;
options: ApiPollOptionJSON[];
emojis: ApiCustomEmojiJSON[];
- voted?: boolean;
- own_votes?: number[];
+ voted: boolean;
+ own_votes: number[];
}
diff --git a/app/javascript/mastodon/api_types/search.ts b/app/javascript/mastodon/api_types/search.ts
deleted file mode 100644
index 795cbb2b41..0000000000
--- a/app/javascript/mastodon/api_types/search.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { ApiAccountJSON } from './accounts';
-import type { ApiStatusJSON } from './statuses';
-import type { ApiHashtagJSON } from './tags';
-
-export type ApiSearchType = 'accounts' | 'statuses' | 'hashtags';
-
-export interface ApiSearchResultsJSON {
- accounts: ApiAccountJSON[];
- statuses: ApiStatusJSON[];
- hashtags: ApiHashtagJSON[];
-}
diff --git a/app/javascript/mastodon/api_types/suggestions.ts b/app/javascript/mastodon/api_types/suggestions.ts
deleted file mode 100644
index 7d91daf901..0000000000
--- a/app/javascript/mastodon/api_types/suggestions.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
-
-export type ApiSuggestionSourceJSON =
- | 'featured'
- | 'most_followed'
- | 'most_interactions'
- | 'similar_to_recently_followed'
- | 'friends_of_friends';
-
-export interface ApiSuggestionJSON {
- sources: [ApiSuggestionSourceJSON, ...ApiSuggestionSourceJSON[]];
- account: ApiAccountJSON;
-}
diff --git a/app/javascript/mastodon/api_types/tags.ts b/app/javascript/mastodon/api_types/tags.ts
deleted file mode 100644
index 0c16c8bd28..0000000000
--- a/app/javascript/mastodon/api_types/tags.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-interface ApiHistoryJSON {
- day: string;
- accounts: string;
- uses: string;
-}
-
-export interface ApiHashtagJSON {
- id: string;
- name: string;
- url: string;
- history: [ApiHistoryJSON, ...ApiHistoryJSON[]];
- following?: boolean;
-}
diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx
new file mode 100644
index 0000000000..6204dcdf35
--- /dev/null
+++ b/app/javascript/mastodon/components/account.jsx
@@ -0,0 +1,194 @@
+import PropTypes from 'prop-types';
+import { useCallback } from 'react';
+
+import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+import { Link } from 'react-router-dom';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
+import { EmptyAccount } from 'mastodon/components/empty_account';
+import { ShortNumber } from 'mastodon/components/short_number';
+import { VerifiedBadge } from 'mastodon/components/verified_badge';
+
+import DropdownMenuContainer from '../containers/dropdown_menu_container';
+import { me } from '../initial_state';
+
+import { Avatar } from './avatar';
+import { Button } from './button';
+import { FollowersCounter } from './counters';
+import { DisplayName } from './display_name';
+import { RelativeTimestamp } from './relative_timestamp';
+
+const messages = defineMessages({
+ follow: { id: 'account.follow', defaultMessage: 'Follow' },
+ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
+ cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
+ unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
+ unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
+ mute_notifications: { id: 'account.mute_notifications_short', defaultMessage: 'Mute notifications' },
+ unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' },
+ mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
+ block: { id: 'account.block_short', defaultMessage: 'Block' },
+ more: { id: 'status.more', defaultMessage: 'More' },
+});
+
+const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifications, hidden, hideButtons, minimal, defaultAction, children, withBio }) => {
+ const intl = useIntl();
+
+ const handleFollow = useCallback(() => {
+ onFollow(account);
+ }, [onFollow, account]);
+
+ const handleBlock = useCallback(() => {
+ onBlock(account);
+ }, [onBlock, account]);
+
+ const handleMute = useCallback(() => {
+ onMute(account);
+ }, [onMute, account]);
+
+ const handleMuteNotifications = useCallback(() => {
+ onMuteNotifications(account, true);
+ }, [onMuteNotifications, account]);
+
+ const handleUnmuteNotifications = useCallback(() => {
+ onMuteNotifications(account, false);
+ }, [onMuteNotifications, account]);
+
+ if (!account) {
+ return ;
+ }
+
+ if (hidden) {
+ return (
+ <>
+ {account.get('display_name')}
+ {account.get('username')}
+ >
+ );
+ }
+
+ let buttons;
+
+ if (!hideButtons && account.get('id') !== me && account.get('relationship', null) !== null) {
+ const following = account.getIn(['relationship', 'following']);
+ const requested = account.getIn(['relationship', 'requested']);
+ const blocking = account.getIn(['relationship', 'blocking']);
+ const muting = account.getIn(['relationship', 'muting']);
+
+ if (requested) {
+ buttons = ;
+ } else if (blocking) {
+ buttons = ;
+ } else if (muting) {
+ let menu;
+
+ if (account.getIn(['relationship', 'muting_notifications'])) {
+ menu = [{ text: intl.formatMessage(messages.unmute_notifications), action: handleUnmuteNotifications }];
+ } else {
+ menu = [{ text: intl.formatMessage(messages.mute_notifications), action: handleMuteNotifications }];
+ }
+
+ buttons = (
+ <>
+
+
+
+ >
+ );
+ } else if (defaultAction === 'mute') {
+ buttons = ;
+ } else if (defaultAction === 'block') {
+ buttons = ;
+ } else if (!account.get('suspended') && !account.get('moved') || following) {
+ buttons = ;
+ }
+ }
+
+ let muteTimeRemaining;
+
+ if (account.get('mute_expires_at')) {
+ muteTimeRemaining = <>· >;
+ }
+
+ let verification;
+
+ const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
+
+ if (firstVerifiedField) {
+ verification = ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ {!minimal && (
+
+ {verification} {muteTimeRemaining}
+
+ )}
+
+
+
+ {!minimal && children && (
+
+
+ {children}
+
+
+ {buttons}
+
+
+ )}
+ {!minimal && !children && (
+
+ {buttons}
+
+ )}
+
+
+ {withBio && (account.get('note').length > 0 ? (
+
+ ) : (
+
+ ))}
+
+ );
+};
+
+Account.propTypes = {
+ size: PropTypes.number,
+ account: ImmutablePropTypes.record,
+ onFollow: PropTypes.func,
+ onBlock: PropTypes.func,
+ onMute: PropTypes.func,
+ onMuteNotifications: PropTypes.func,
+ hidden: PropTypes.bool,
+ hideButtons: PropTypes.bool,
+ minimal: PropTypes.bool,
+ defaultAction: PropTypes.string,
+ withBio: PropTypes.bool,
+ children: PropTypes.any,
+};
+
+export default Account;
diff --git a/app/javascript/mastodon/components/account.tsx b/app/javascript/mastodon/components/account.tsx
deleted file mode 100644
index c6c2204085..0000000000
--- a/app/javascript/mastodon/components/account.tsx
+++ /dev/null
@@ -1,299 +0,0 @@
-import type { ReactNode } from 'react';
-import type React from 'react';
-import { useCallback, useMemo } from 'react';
-
-import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
-
-import classNames from 'classnames';
-import { Link } from 'react-router-dom';
-
-import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
-import {
- blockAccount,
- unblockAccount,
- muteAccount,
- unmuteAccount,
-} from 'mastodon/actions/accounts';
-import { openModal } from 'mastodon/actions/modal';
-import { initMuteModal } from 'mastodon/actions/mutes';
-import { Avatar } from 'mastodon/components/avatar';
-import { Button } from 'mastodon/components/button';
-import { FollowersCounter } from 'mastodon/components/counters';
-import { DisplayName } from 'mastodon/components/display_name';
-import { Dropdown } from 'mastodon/components/dropdown_menu';
-import { FollowButton } from 'mastodon/components/follow_button';
-import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
-import { ShortNumber } from 'mastodon/components/short_number';
-import { Skeleton } from 'mastodon/components/skeleton';
-import { VerifiedBadge } from 'mastodon/components/verified_badge';
-import type { MenuItem } from 'mastodon/models/dropdown_menu';
-import { useAppSelector, useAppDispatch } from 'mastodon/store';
-
-const messages = defineMessages({
- follow: { id: 'account.follow', defaultMessage: 'Follow' },
- unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
- cancel_follow_request: {
- id: 'account.cancel_follow_request',
- defaultMessage: 'Withdraw follow request',
- },
- unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
- unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
- mute_notifications: {
- id: 'account.mute_notifications_short',
- defaultMessage: 'Mute notifications',
- },
- unmute_notifications: {
- id: 'account.unmute_notifications_short',
- defaultMessage: 'Unmute notifications',
- },
- mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
- block: { id: 'account.block_short', defaultMessage: 'Block' },
- more: { id: 'status.more', defaultMessage: 'More' },
- addToLists: {
- id: 'account.add_or_remove_from_list',
- defaultMessage: 'Add or Remove from lists',
- },
- openOriginalPage: {
- id: 'account.open_original_page',
- defaultMessage: 'Open original page',
- },
-});
-
-export const Account: React.FC<{
- size?: number;
- id: string;
- hidden?: boolean;
- minimal?: boolean;
- defaultAction?: 'block' | 'mute';
- withBio?: boolean;
- hideButtons?: boolean;
- children?: ReactNode;
-}> = ({
- id,
- size = 46,
- hidden,
- minimal,
- defaultAction,
- withBio,
- hideButtons,
- children,
-}) => {
- const intl = useIntl();
- const account = useAppSelector((state) => state.accounts.get(id));
- const relationship = useAppSelector((state) => state.relationships.get(id));
- const dispatch = useAppDispatch();
- const accountUrl = account?.url;
-
- const handleBlock = useCallback(() => {
- if (relationship?.blocking) {
- dispatch(unblockAccount(id));
- } else {
- dispatch(blockAccount(id));
- }
- }, [dispatch, id, relationship]);
-
- const handleMute = useCallback(() => {
- if (relationship?.muting) {
- dispatch(unmuteAccount(id));
- } else {
- dispatch(initMuteModal(account));
- }
- }, [dispatch, id, account, relationship]);
-
- const menu = useMemo(() => {
- let arr: MenuItem[] = [];
-
- if (defaultAction === 'mute') {
- const handleMuteNotifications = () => {
- dispatch(muteAccount(id, true));
- };
-
- const handleUnmuteNotifications = () => {
- dispatch(muteAccount(id, false));
- };
-
- arr = [
- {
- text: intl.formatMessage(
- relationship?.muting_notifications
- ? messages.unmute_notifications
- : messages.mute_notifications,
- ),
- action: relationship?.muting_notifications
- ? handleUnmuteNotifications
- : handleMuteNotifications,
- },
- ];
- } else if (defaultAction !== 'block') {
- const handleAddToLists = () => {
- dispatch(
- openModal({
- modalType: 'LIST_ADDER',
- modalProps: {
- accountId: id,
- },
- }),
- );
- };
-
- arr = [
- {
- text: intl.formatMessage(messages.addToLists),
- action: handleAddToLists,
- },
- ];
-
- if (accountUrl) {
- arr.unshift(
- {
- text: intl.formatMessage(messages.openOriginalPage),
- href: accountUrl,
- },
- null,
- );
- }
- }
-
- return arr;
- }, [dispatch, intl, id, accountUrl, relationship, defaultAction]);
-
- if (hidden) {
- return (
- <>
- {account?.display_name}
- {account?.username}
- >
- );
- }
-
- let button: React.ReactNode, dropdown: React.ReactNode;
-
- if (menu.length > 0) {
- dropdown = (
-
- );
- }
-
- if (defaultAction === 'block') {
- button = (
-
- );
- } else if (defaultAction === 'mute') {
- button = (
-
- );
- } else {
- button = ;
- }
-
- if (hideButtons) {
- button = null;
- }
-
- let muteTimeRemaining: React.ReactNode;
-
- if (account?.mute_expires_at) {
- muteTimeRemaining = (
- <>
- ·
- >
- );
- }
-
- let verification: React.ReactNode;
-
- const firstVerifiedField = account?.fields.find((item) => !!item.verified_at);
-
- if (firstVerifiedField) {
- verification = ;
- }
-
- return (
-
-
-
-
- {account ? (
-
- ) : (
-
- )}
-
-
-
-
-
- {!minimal && (
-
- {account ? (
- <>
- {' '}
- {verification} {muteTimeRemaining}
- >
- ) : (
-
- )}
-
- )}
-
-
-
- {!minimal && children && (
-
-
{children}
-
- {dropdown}
- {button}
-
-
- )}
- {!minimal && !children && (
-
- {dropdown}
- {button}
-
- )}
-
-
- {account &&
- withBio &&
- (account.note.length > 0 ? (
-
- ) : (
-
-
-
- ))}
-
- );
-};
diff --git a/app/javascript/mastodon/components/account_bio.tsx b/app/javascript/mastodon/components/account_bio.tsx
index 301ffcbb24..9d523c7402 100644
--- a/app/javascript/mastodon/components/account_bio.tsx
+++ b/app/javascript/mastodon/components/account_bio.tsx
@@ -1,4 +1,4 @@
-import { useLinks } from 'mastodon/hooks/useLinks';
+import { useLinks } from 'mastodon/../hooks/useLinks';
export const AccountBio: React.FC<{
note: string;
diff --git a/app/javascript/mastodon/components/account_fields.tsx b/app/javascript/mastodon/components/account_fields.tsx
index 4ce55f7896..e297f99e3a 100644
--- a/app/javascript/mastodon/components/account_fields.tsx
+++ b/app/javascript/mastodon/components/account_fields.tsx
@@ -1,8 +1,8 @@
import classNames from 'classnames';
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
+import { useLinks } from 'mastodon/../hooks/useLinks';
import { Icon } from 'mastodon/components/icon';
-import { useLinks } from 'mastodon/hooks/useLinks';
import type { Account } from 'mastodon/models/account';
export const AccountFields: React.FC<{
diff --git a/app/javascript/mastodon/components/alerts_controller.tsx b/app/javascript/mastodon/components/alerts_controller.tsx
deleted file mode 100644
index 26749fa103..0000000000
--- a/app/javascript/mastodon/components/alerts_controller.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { useState, useEffect } from 'react';
-
-import { useIntl } from 'react-intl';
-import type { IntlShape } from 'react-intl';
-
-import classNames from 'classnames';
-
-import { dismissAlert } from 'mastodon/actions/alerts';
-import type {
- Alert,
- TranslatableString,
- TranslatableValues,
-} from 'mastodon/models/alert';
-import { useAppSelector, useAppDispatch } from 'mastodon/store';
-
-const formatIfNeeded = (
- intl: IntlShape,
- message: TranslatableString,
- values?: TranslatableValues,
-) => {
- if (typeof message === 'object') {
- return intl.formatMessage(message, values);
- }
-
- return message;
-};
-
-const Alert: React.FC<{
- alert: Alert;
- dismissAfter: number;
-}> = ({
- alert: { key, title, message, values, action, onClick },
- dismissAfter,
-}) => {
- const dispatch = useAppDispatch();
- const intl = useIntl();
- const [active, setActive] = useState(false);
-
- useEffect(() => {
- const setActiveTimeout = setTimeout(() => {
- setActive(true);
- }, 1);
-
- return () => {
- clearTimeout(setActiveTimeout);
- };
- }, []);
-
- useEffect(() => {
- const dismissTimeout = setTimeout(() => {
- setActive(false);
-
- // Allow CSS transition to finish before removing from the DOM
- setTimeout(() => {
- dispatch(dismissAlert({ key }));
- }, 500);
- }, dismissAfter);
-
- return () => {
- clearTimeout(dismissTimeout);
- };
- }, [dispatch, setActive, key, dismissAfter]);
-
- return (
-
-
- {title && (
-
- {formatIfNeeded(intl, title, values)}
-
- )}
-
-
- {formatIfNeeded(intl, message, values)}
-
-
- {action && (
-
- )}
-
-
- );
-};
-
-export const AlertsController: React.FC = () => {
- const alerts = useAppSelector((state) => state.alerts);
-
- if (alerts.length === 0) {
- return null;
- }
-
- return (
-
- {alerts.map((alert, idx) => (
-
- ))}
-
- );
-};
diff --git a/app/javascript/mastodon/components/alt_text_badge.tsx b/app/javascript/mastodon/components/alt_text_badge.tsx
index 701cfbe8b4..99bec1ee51 100644
--- a/app/javascript/mastodon/components/alt_text_badge.tsx
+++ b/app/javascript/mastodon/components/alt_text_badge.tsx
@@ -1,4 +1,4 @@
-import { useState, useCallback, useRef, useId } from 'react';
+import { useState, useCallback, useRef } from 'react';
import { FormattedMessage } from 'react-intl';
@@ -8,15 +8,12 @@ import type {
UsePopperOptions,
} from 'react-overlays/esm/usePopper';
-import { useSelectableClick } from 'mastodon/hooks/useSelectableClick';
-
const offset = [0, 4] as OffsetValue;
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
export const AltTextBadge: React.FC<{
description: string;
}> = ({ description }) => {
- const accessibilityId = useId();
const anchorRef = useRef(null);
const [open, setOpen] = useState(false);
@@ -28,16 +25,12 @@ export const AltTextBadge: React.FC<{
setOpen(false);
}, [setOpen]);
- const [handleMouseDown, handleMouseUp] = useSelectableClick(handleClose);
-
return (
<>
@@ -54,12 +47,9 @@ export const AltTextBadge: React.FC<{
>
{({ props }) => (
-
diff --git a/app/javascript/mastodon/components/filter_warning.tsx b/app/javascript/mastodon/components/filter_warning.tsx
index 5eaaac4ba3..4305e43038 100644
--- a/app/javascript/mastodon/components/filter_warning.tsx
+++ b/app/javascript/mastodon/components/filter_warning.tsx
@@ -10,16 +10,13 @@ export const FilterWarning: React.FC<{
{chunks},
- }}
+ defaultMessage='Matches filter “{title}”'
+ values={{ title }}
/>
diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx
index f21ad60240..4414c75a84 100644
--- a/app/javascript/mastodon/components/follow_button.tsx
+++ b/app/javascript/mastodon/components/follow_button.tsx
@@ -2,8 +2,6 @@ import { useCallback, useEffect } from 'react';
import { useIntl, defineMessages } from 'react-intl';
-import classNames from 'classnames';
-
import { useIdentity } from '@/mastodon/identity_context';
import { fetchRelationships, followAccount } from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal';
@@ -16,13 +14,13 @@ const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' },
- editProfile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
+ mutual: { id: 'account.mutual', defaultMessage: 'Mutual' },
+ edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
});
export const FollowButton: React.FC<{
accountId?: string;
- compact?: boolean;
-}> = ({ accountId, compact }) => {
+}> = ({ accountId }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { signedIn } = useIdentity();
@@ -54,7 +52,7 @@ export const FollowButton: React.FC<{
);
}
- if (!relationship || !accountId) return;
+ if (!relationship) return;
if (accountId === me) {
return;
@@ -72,9 +70,15 @@ export const FollowButton: React.FC<{
if (!signedIn) {
label = intl.formatMessage(messages.follow);
} else if (accountId === me) {
- label = intl.formatMessage(messages.editProfile);
+ label = intl.formatMessage(messages.edit_profile);
} else if (!relationship) {
label = ;
+ } else if (
+ relationship.following &&
+ isShowItem('relationships') &&
+ relationship.followed_by
+ ) {
+ label = intl.formatMessage(messages.mutual);
} else if (relationship.following || relationship.requested) {
label = intl.formatMessage(messages.unfollow);
} else if (relationship.followed_by && isShowItem('relationships')) {
@@ -88,10 +92,8 @@ export const FollowButton: React.FC<{
{label}
@@ -101,14 +103,8 @@ export const FollowButton: React.FC<{
return (
+ );
+};
diff --git a/app/javascript/mastodon/components/hashtag.tsx b/app/javascript/mastodon/components/hashtag.tsx
index 346c95183f..8963e4a40d 100644
--- a/app/javascript/mastodon/components/hashtag.tsx
+++ b/app/javascript/mastodon/components/hashtag.tsx
@@ -12,7 +12,6 @@ import { Sparklines, SparklinesCurve } from 'react-sparklines';
import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton';
-import type { Hashtag as HashtagType } from 'mastodon/models/tags';
interface SilentErrorBoundaryProps {
children: React.ReactNode;
@@ -81,32 +80,15 @@ export const ImmutableHashtag = ({ hashtag }: ImmutableHashtagProps) => (
/>
);
-export const CompatibilityHashtag: React.FC<{
- hashtag: HashtagType;
-}> = ({ hashtag }) => (
- (day.uses as unknown as number) * 1)
- .reverse()}
- />
-);
-
export interface HashtagProps {
className?: string;
description?: React.ReactNode;
history?: number[];
name: string;
- people?: number;
+ people: number;
to: string;
uses?: number;
withGraph?: boolean;
- children?: React.ReactNode;
}
export const Hashtag: React.FC = ({
@@ -118,7 +100,6 @@ export const Hashtag: React.FC = ({
className,
description,
withGraph = true,
- children,
}) => (
@@ -153,14 +134,12 @@ export const Hashtag: React.FC = ({
0)}
+ data={history ? history : Array.from(Array(7)).map(() => 0)}
>
)}
-
- {children &&
{children}
}
);
diff --git a/app/javascript/mastodon/components/hashtag_bar.tsx b/app/javascript/mastodon/components/hashtag_bar.tsx
index ce8f17ddb9..9e1d74bb74 100644
--- a/app/javascript/mastodon/components/hashtag_bar.tsx
+++ b/app/javascript/mastodon/components/hashtag_bar.tsx
@@ -20,7 +20,6 @@ export type StatusLike = Record<{
contentHTML: string;
media_attachments: List;
spoiler_text?: string;
- account: Record<{ id: string }>;
}>;
function normalizeHashtag(hashtag: string) {
@@ -196,36 +195,19 @@ export function getHashtagBarForStatus(status: StatusLike) {
return {
statusContentProps,
- hashtagBar: (
-
- ),
+ hashtagBar: ,
};
}
-export function getFeaturedHashtagBar(
- accountId: string,
- acct: string,
- tags: string[],
-) {
- return (
-
- );
+export function getFeaturedHashtagBar(acct: string, tags: string[]) {
+ return ;
}
const HashtagBar: React.FC<{
hashtags: string[];
- accountId: string;
acct?: string;
defaultExpanded?: boolean;
-}> = ({ hashtags, accountId, acct, defaultExpanded }) => {
+}> = ({ hashtags, acct, defaultExpanded }) => {
const [expanded, setExpanded] = useState(false);
const handleClick = useCallback(() => {
setExpanded(true);
@@ -246,7 +228,6 @@ const HashtagBar: React.FC<{
#{hashtag}
diff --git a/app/javascript/mastodon/components/hover_card_controller.tsx b/app/javascript/mastodon/components/hover_card_controller.tsx
index 38c3306f30..057ef1aaed 100644
--- a/app/javascript/mastodon/components/hover_card_controller.tsx
+++ b/app/javascript/mastodon/components/hover_card_controller.tsx
@@ -8,8 +8,8 @@ import type {
UsePopperOptions,
} from 'react-overlays/esm/usePopper';
+import { useTimeout } from 'mastodon/../hooks/useTimeout';
import { HoverCardAccount } from 'mastodon/components/hover_card_account';
-import { useTimeout } from 'mastodon/hooks/useTimeout';
const offset = [-12, 4] as OffsetValue;
const enterDelay = 750;
diff --git a/app/javascript/mastodon/components/icon_button.tsx b/app/javascript/mastodon/components/icon_button.tsx
index 7e0b3e7a22..b7cac35960 100644
--- a/app/javascript/mastodon/components/icon_button.tsx
+++ b/app/javascript/mastodon/components/icon_button.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback, forwardRef } from 'react';
+import { PureComponent, createRef } from 'react';
import classNames from 'classnames';
@@ -15,110 +15,101 @@ interface Props {
onMouseDown?: React.MouseEventHandler;
onKeyDown?: React.KeyboardEventHandler;
onKeyPress?: React.KeyboardEventHandler;
- active?: boolean;
+ active: boolean;
expanded?: boolean;
style?: React.CSSProperties;
activeStyle?: React.CSSProperties;
- disabled?: boolean;
+ disabled: boolean;
inverted?: boolean;
- animate?: boolean;
- overlay?: boolean;
- tabIndex?: number;
+ animate: boolean;
+ overlay: boolean;
+ tabIndex: number;
counter?: number;
href?: string;
- ariaHidden?: boolean;
+ ariaHidden: boolean;
data_id?: string;
}
+interface States {
+ activate: boolean;
+ deactivate: boolean;
+}
+export class IconButton extends PureComponent {
+ buttonRef = createRef();
-export const IconButton = forwardRef(
- (
- {
+ static defaultProps = {
+ active: false,
+ disabled: false,
+ animate: false,
+ overlay: false,
+ tabIndex: 0,
+ ariaHidden: false,
+ };
+
+ state = {
+ activate: false,
+ deactivate: false,
+ };
+
+ UNSAFE_componentWillReceiveProps(nextProps: Props) {
+ if (!nextProps.animate) return;
+
+ if (this.props.active && !nextProps.active) {
+ this.setState({ activate: false, deactivate: true });
+ } else if (!this.props.active && nextProps.active) {
+ this.setState({ activate: true, deactivate: false });
+ }
+ }
+
+ handleClick: React.MouseEventHandler = (e) => {
+ e.preventDefault();
+
+ if (!this.props.disabled && this.props.onClick != null) {
+ this.props.onClick(e);
+ }
+ };
+
+ handleKeyPress: React.KeyboardEventHandler = (e) => {
+ if (this.props.onKeyPress && !this.props.disabled) {
+ this.props.onKeyPress(e);
+ }
+ };
+
+ handleMouseDown: React.MouseEventHandler = (e) => {
+ if (!this.props.disabled && this.props.onMouseDown) {
+ this.props.onMouseDown(e);
+ }
+ };
+
+ handleKeyDown: React.KeyboardEventHandler = (e) => {
+ if (!this.props.disabled && this.props.onKeyDown) {
+ this.props.onKeyDown(e);
+ }
+ };
+
+ render() {
+ const style = {
+ ...this.props.style,
+ ...(this.props.active ? this.props.activeStyle : {}),
+ };
+
+ const {
+ active,
className,
+ disabled,
expanded,
icon,
iconComponent,
inverted,
+ overlay,
+ tabIndex,
title,
counter,
href,
- style,
- activeStyle,
- onClick,
- onKeyDown,
- onKeyPress,
- onMouseDown,
- active = false,
- disabled = false,
- animate = false,
- overlay = false,
- tabIndex = 0,
- ariaHidden = false,
- data_id = undefined,
- },
- buttonRef,
- ) => {
- const [activate, setActivate] = useState(false);
- const [deactivate, setDeactivate] = useState(false);
+ ariaHidden,
+ data_id,
+ } = this.props;
- useEffect(() => {
- if (!animate) {
- return;
- }
-
- if (activate && !active) {
- setActivate(false);
- setDeactivate(true);
- } else if (!activate && active) {
- setActivate(true);
- setDeactivate(false);
- }
- }, [setActivate, setDeactivate, animate, active, activate]);
-
- const handleClick: React.MouseEventHandler = useCallback(
- (e) => {
- e.preventDefault();
-
- if (!disabled) {
- onClick?.(e);
- }
- },
- [disabled, onClick],
- );
-
- const handleKeyPress: React.KeyboardEventHandler =
- useCallback(
- (e) => {
- if (!disabled) {
- onKeyPress?.(e);
- }
- },
- [disabled, onKeyPress],
- );
-
- const handleMouseDown: React.MouseEventHandler =
- useCallback(
- (e) => {
- if (!disabled) {
- onMouseDown?.(e);
- }
- },
- [disabled, onMouseDown],
- );
-
- const handleKeyDown: React.KeyboardEventHandler =
- useCallback(
- (e) => {
- if (!disabled) {
- onKeyDown?.(e);
- }
- },
- [disabled, onKeyDown],
- );
-
- const buttonStyle = {
- ...style,
- ...(active ? activeStyle : {}),
- };
+ const { activate, deactivate } = this.state;
const classes = classNames(className, 'icon-button', {
active,
@@ -157,20 +148,18 @@ export const IconButton = forwardRef(
aria-hidden={ariaHidden}
title={title}
className={classes}
- onClick={handleClick}
- onMouseDown={handleMouseDown}
- onKeyDown={handleKeyDown}
- onKeyPress={handleKeyPress} // eslint-disable-line @typescript-eslint/no-deprecated
- style={buttonStyle}
+ onClick={this.handleClick}
+ onMouseDown={this.handleMouseDown}
+ onKeyDown={this.handleKeyDown}
+ onKeyPress={this.handleKeyPress}
+ style={style}
tabIndex={tabIndex}
disabled={disabled}
data-id={data_id}
- ref={buttonRef}
+ ref={this.buttonRef}
>
{contents}
);
- },
-);
-
-IconButton.displayName = 'IconButton';
+ }
+}
diff --git a/app/javascript/mastodon/components/load_gap.tsx b/app/javascript/mastodon/components/load_gap.tsx
index 6cbdee6ce5..544b5e1461 100644
--- a/app/javascript/mastodon/components/load_gap.tsx
+++ b/app/javascript/mastodon/components/load_gap.tsx
@@ -1,10 +1,9 @@
-import { useCallback, useState } from 'react';
+import { useCallback } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { Icon } from 'mastodon/components/icon';
-import { LoadingIndicator } from 'mastodon/components/loading_indicator';
const messages = defineMessages({
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
@@ -18,12 +17,10 @@ interface Props {
export const LoadGap = ({ disabled, param, onClick }: Props) => {
const intl = useIntl();
- const [loading, setLoading] = useState(false);
const handleClick = useCallback(() => {
- setLoading(true);
onClick(param);
- }, [setLoading, param, onClick]);
+ }, [param, onClick]);
return (
);
};
diff --git a/app/javascript/mastodon/components/media_gallery.jsx b/app/javascript/mastodon/components/media_gallery.jsx
index 12cf381e5e..443d2cb8bd 100644
--- a/app/javascript/mastodon/components/media_gallery.jsx
+++ b/app/javascript/mastodon/components/media_gallery.jsx
@@ -12,7 +12,6 @@ import { debounce } from 'lodash';
import { AltTextBadge } from 'mastodon/components/alt_text_badge';
import { Blurhash } from 'mastodon/components/blurhash';
-import { SpoilerButton } from 'mastodon/components/spoiler_button';
import { formatTime } from 'mastodon/features/video';
import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
@@ -39,7 +38,6 @@ class Item extends PureComponent {
state = {
loaded: false,
- error: false,
};
handleMouseEnter = (e) => {
@@ -83,10 +81,6 @@ class Item extends PureComponent {
this.setState({ loaded: true });
};
- handleImageError = () => {
- this.setState({ error: true });
- };
-
render () {
const { attachment, lang, index, size, standalone, displayWidth, visible } = this.props;
@@ -119,16 +113,16 @@ class Item extends PureComponent {
width = 25;
}
- const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
-
- if (description?.length > 0) {
- badges.push();
+ if (attachment.get('description')?.length > 0) {
+ badges.push();
}
+ const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
+
if (attachment.get('type') === 'unknown') {
return (
-
+
);
@@ -189,6 +183,7 @@ class Item extends PureComponent {