Merge branch 'develop' into 'preserve-spoilers'
# Conflicts: # app/soapbox/actions/compose.ts
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { importEntities } from 'soapbox/entity-store/actions';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { selectAccount } from 'soapbox/selectors';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features';
|
||||
|
||||
@@ -141,9 +142,9 @@ const fetchAccount = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchRelationships([id]));
|
||||
|
||||
const account = getState().accounts.get(id);
|
||||
const account = selectAccount(getState(), id);
|
||||
|
||||
if (account && !account.get('should_refetch')) {
|
||||
if (account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { defineMessages } from 'react-intl';
|
||||
|
||||
import { fetchRelationships } from 'soapbox/actions/accounts';
|
||||
import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer';
|
||||
import { accountIdsToAccts } from 'soapbox/selectors';
|
||||
import toast from 'soapbox/toast';
|
||||
import { filterBadges, getTagDiff } from 'soapbox/utils/badges';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
@@ -74,14 +75,6 @@ const ADMIN_REMOVE_PERMISSION_GROUP_REQUEST = 'ADMIN_REMOVE_PERMISSION_GROUP_REQ
|
||||
const ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS = 'ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS';
|
||||
const ADMIN_REMOVE_PERMISSION_GROUP_FAIL = 'ADMIN_REMOVE_PERMISSION_GROUP_FAIL';
|
||||
|
||||
const ADMIN_USERS_SUGGEST_REQUEST = 'ADMIN_USERS_SUGGEST_REQUEST';
|
||||
const ADMIN_USERS_SUGGEST_SUCCESS = 'ADMIN_USERS_SUGGEST_SUCCESS';
|
||||
const ADMIN_USERS_SUGGEST_FAIL = 'ADMIN_USERS_SUGGEST_FAIL';
|
||||
|
||||
const ADMIN_USERS_UNSUGGEST_REQUEST = 'ADMIN_USERS_UNSUGGEST_REQUEST';
|
||||
const ADMIN_USERS_UNSUGGEST_SUCCESS = 'ADMIN_USERS_UNSUGGEST_SUCCESS';
|
||||
const ADMIN_USERS_UNSUGGEST_FAIL = 'ADMIN_USERS_UNSUGGEST_FAIL';
|
||||
|
||||
const ADMIN_USER_INDEX_EXPAND_FAIL = 'ADMIN_USER_INDEX_EXPAND_FAIL';
|
||||
const ADMIN_USER_INDEX_EXPAND_REQUEST = 'ADMIN_USER_INDEX_EXPAND_REQUEST';
|
||||
const ADMIN_USER_INDEX_EXPAND_SUCCESS = 'ADMIN_USER_INDEX_EXPAND_SUCCESS';
|
||||
@@ -121,8 +114,6 @@ const messages = defineMessages({
|
||||
announcementUpdateSuccess: { id: 'admin.edit_announcement.updated', defaultMessage: 'Announcement edited' },
|
||||
});
|
||||
|
||||
const nicknamesFromIds = (getState: () => RootState, ids: string[]) => ids.map(id => getState().accounts.get(id)!.acct);
|
||||
|
||||
const fetchConfig = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST });
|
||||
@@ -329,7 +320,7 @@ const deactivateMastodonUsers = (accountIds: string[], reportId?: string) =>
|
||||
|
||||
const deactivatePleromaUsers = (accountIds: string[]) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||
const nicknames = accountIdsToAccts(getState(), accountIds);
|
||||
return api(getState)
|
||||
.patch('/api/v1/pleroma/admin/users/deactivate', { nicknames })
|
||||
.then(({ data: { users } }) => {
|
||||
@@ -357,7 +348,7 @@ const deactivateUsers = (accountIds: string[], reportId?: string) =>
|
||||
|
||||
const deleteUsers = (accountIds: string[]) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||
const nicknames = accountIdsToAccts(getState(), accountIds);
|
||||
dispatch({ type: ADMIN_USERS_DELETE_REQUEST, accountIds });
|
||||
return api(getState)
|
||||
.delete('/api/v1/pleroma/admin/users', { data: { nicknames } })
|
||||
@@ -382,7 +373,7 @@ const approveMastodonUsers = (accountIds: string[]) =>
|
||||
|
||||
const approvePleromaUsers = (accountIds: string[]) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||
const nicknames = accountIdsToAccts(getState(), accountIds);
|
||||
return api(getState)
|
||||
.patch('/api/v1/pleroma/admin/users/approve', { nicknames })
|
||||
.then(({ data: { users } }) => {
|
||||
@@ -447,7 +438,7 @@ const fetchModerationLog = (params?: Record<string, any>) =>
|
||||
|
||||
const tagUsers = (accountIds: string[], tags: string[]) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||
const nicknames = accountIdsToAccts(getState(), accountIds);
|
||||
dispatch({ type: ADMIN_USERS_TAG_REQUEST, accountIds, tags });
|
||||
return api(getState)
|
||||
.put('/api/v1/pleroma/admin/users/tag', { nicknames, tags })
|
||||
@@ -460,7 +451,7 @@ const tagUsers = (accountIds: string[], tags: string[]) =>
|
||||
|
||||
const untagUsers = (accountIds: string[], tags: string[]) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||
const nicknames = accountIdsToAccts(getState(), accountIds);
|
||||
|
||||
// Legacy: allow removing legacy 'donor' tags.
|
||||
if (tags.includes('badge:donor')) {
|
||||
@@ -495,17 +486,9 @@ const setBadges = (accountId: string, oldTags: string[], newTags: string[]) =>
|
||||
return dispatch(setTags(accountId, oldBadges, newBadges));
|
||||
};
|
||||
|
||||
const verifyUser = (accountId: string) =>
|
||||
(dispatch: AppDispatch) =>
|
||||
dispatch(tagUsers([accountId], ['verified']));
|
||||
|
||||
const unverifyUser = (accountId: string) =>
|
||||
(dispatch: AppDispatch) =>
|
||||
dispatch(untagUsers([accountId], ['verified']));
|
||||
|
||||
const addPermission = (accountIds: string[], permissionGroup: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||
const nicknames = accountIdsToAccts(getState(), accountIds);
|
||||
dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup });
|
||||
return api(getState)
|
||||
.post(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { nicknames })
|
||||
@@ -518,7 +501,7 @@ const addPermission = (accountIds: string[], permissionGroup: string) =>
|
||||
|
||||
const removePermission = (accountIds: string[], permissionGroup: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||
const nicknames = accountIdsToAccts(getState(), accountIds);
|
||||
dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup });
|
||||
return api(getState)
|
||||
.delete(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { data: { nicknames } })
|
||||
@@ -562,32 +545,6 @@ const setRole = (accountId: string, role: 'user' | 'moderator' | 'admin') =>
|
||||
}
|
||||
};
|
||||
|
||||
const suggestUsers = (accountIds: string[]) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||
dispatch({ type: ADMIN_USERS_SUGGEST_REQUEST, accountIds });
|
||||
return api(getState)
|
||||
.patch('/api/v1/pleroma/admin/users/suggest', { nicknames })
|
||||
.then(({ data: { users } }) => {
|
||||
dispatch({ type: ADMIN_USERS_SUGGEST_SUCCESS, users, accountIds });
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_USERS_SUGGEST_FAIL, error, accountIds });
|
||||
});
|
||||
};
|
||||
|
||||
const unsuggestUsers = (accountIds: string[]) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||
dispatch({ type: ADMIN_USERS_UNSUGGEST_REQUEST, accountIds });
|
||||
return api(getState)
|
||||
.patch('/api/v1/pleroma/admin/users/unsuggest', { nicknames })
|
||||
.then(({ data: { users } }) => {
|
||||
dispatch({ type: ADMIN_USERS_UNSUGGEST_SUCCESS, users, accountIds });
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_USERS_UNSUGGEST_FAIL, error, accountIds });
|
||||
});
|
||||
};
|
||||
|
||||
const setUserIndexQuery = (query: string) => ({ type: ADMIN_USER_INDEX_QUERY_SET, query });
|
||||
|
||||
const fetchUserIndex = () =>
|
||||
@@ -765,12 +722,6 @@ export {
|
||||
ADMIN_REMOVE_PERMISSION_GROUP_REQUEST,
|
||||
ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS,
|
||||
ADMIN_REMOVE_PERMISSION_GROUP_FAIL,
|
||||
ADMIN_USERS_SUGGEST_REQUEST,
|
||||
ADMIN_USERS_SUGGEST_SUCCESS,
|
||||
ADMIN_USERS_SUGGEST_FAIL,
|
||||
ADMIN_USERS_UNSUGGEST_REQUEST,
|
||||
ADMIN_USERS_UNSUGGEST_SUCCESS,
|
||||
ADMIN_USERS_UNSUGGEST_FAIL,
|
||||
ADMIN_USER_INDEX_EXPAND_FAIL,
|
||||
ADMIN_USER_INDEX_EXPAND_REQUEST,
|
||||
ADMIN_USER_INDEX_EXPAND_SUCCESS,
|
||||
@@ -811,16 +762,12 @@ export {
|
||||
untagUsers,
|
||||
setTags,
|
||||
setBadges,
|
||||
verifyUser,
|
||||
unverifyUser,
|
||||
addPermission,
|
||||
removePermission,
|
||||
promoteToAdmin,
|
||||
promoteToModerator,
|
||||
demoteToUser,
|
||||
setRole,
|
||||
suggestUsers,
|
||||
unsuggestUsers,
|
||||
setUserIndexQuery,
|
||||
fetchUserIndex,
|
||||
expandUserIndex,
|
||||
|
||||
@@ -16,6 +16,7 @@ import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth';
|
||||
import { startOnboarding } from 'soapbox/actions/onboarding';
|
||||
import { custom } from 'soapbox/custom';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { selectAccount } from 'soapbox/selectors';
|
||||
import KVStore from 'soapbox/storage/kv-store';
|
||||
import toast from 'soapbox/toast';
|
||||
import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth';
|
||||
@@ -227,7 +228,7 @@ export const logOut = () =>
|
||||
|
||||
export const switchAccount = (accountId: string, background = false) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const account = getState().accounts.get(accountId);
|
||||
const account = selectAccount(getState(), accountId);
|
||||
// Clear all stored cache from React Query
|
||||
queryClient.invalidateQueries();
|
||||
queryClient.clear();
|
||||
@@ -239,7 +240,7 @@ export const fetchOwnAccounts = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
return state.auth.users.forEach((user) => {
|
||||
const account = state.accounts.get(user.id);
|
||||
const account = selectAccount(state, user.id);
|
||||
if (!account) {
|
||||
dispatch(verifyCredentials(user.access_token, user.url))
|
||||
.catch(() => console.warn(`Failed to load account: ${user.url}`));
|
||||
|
||||
@@ -6,6 +6,7 @@ import { defineMessages, IntlShape } from 'react-intl';
|
||||
import api from 'soapbox/api';
|
||||
import { isNativeEmoji } from 'soapbox/features/emoji';
|
||||
import emojiSearch from 'soapbox/features/emoji/search';
|
||||
import { selectAccount, selectOwnAccount } from 'soapbox/selectors';
|
||||
import { tagHistory } from 'soapbox/settings';
|
||||
import toast from 'soapbox/toast';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
@@ -153,12 +154,15 @@ const replyCompose = (status: Status) =>
|
||||
const instance = state.instance;
|
||||
const { explicitAddressing } = getFeatures(instance);
|
||||
const preserveSpoilers = !!getSettings(state).get('preserveSpoilers');
|
||||
const account = selectOwnAccount(state);
|
||||
|
||||
if (!account) return;
|
||||
|
||||
const action: ComposeReplyAction = {
|
||||
type: COMPOSE_REPLY,
|
||||
id: 'compose-modal',
|
||||
status: status,
|
||||
account: state.accounts.get(state.me)!,
|
||||
account,
|
||||
explicitAddressing,
|
||||
preserveSpoilers,
|
||||
};
|
||||
@@ -190,7 +194,7 @@ const quoteCompose = (status: Status) =>
|
||||
type: COMPOSE_QUOTE,
|
||||
id: 'compose-modal',
|
||||
status: status,
|
||||
account: state.accounts.get(state.me),
|
||||
account: selectOwnAccount(state),
|
||||
explicitAddressing,
|
||||
};
|
||||
|
||||
@@ -254,7 +258,7 @@ const directCompose = (account: Account) =>
|
||||
|
||||
const directComposeById = (accountId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const account = getState().accounts.get(accountId);
|
||||
const account = selectAccount(getState(), accountId);
|
||||
if (!account) return;
|
||||
|
||||
const action: ComposeDirectAction = {
|
||||
@@ -644,7 +648,7 @@ interface ComposeSuggestionSelectAction {
|
||||
|
||||
const selectComposeSuggestion = (composeId: string, position: number, token: string | null, suggestion: AutoSuggestion, path: Array<string | number>) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
let completion, startPosition;
|
||||
let completion = '', startPosition = position;
|
||||
|
||||
if (typeof suggestion === 'object' && suggestion.id) {
|
||||
completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons;
|
||||
@@ -654,8 +658,8 @@ const selectComposeSuggestion = (composeId: string, position: number, token: str
|
||||
} else if (typeof suggestion === 'string' && suggestion[0] === '#') {
|
||||
completion = suggestion;
|
||||
startPosition = position - 1;
|
||||
} else {
|
||||
completion = getState().accounts.get(suggestion)!.acct;
|
||||
} else if (typeof suggestion === 'string') {
|
||||
completion = selectAccount(getState(), suggestion)!.acct;
|
||||
startPosition = position;
|
||||
}
|
||||
|
||||
@@ -801,12 +805,13 @@ interface ComposeAddToMentionsAction {
|
||||
const addToMentions = (composeId: string, accountId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const acct = state.accounts.get(accountId)!.acct;
|
||||
const account = selectAccount(state, accountId);
|
||||
if (!account) return;
|
||||
|
||||
const action: ComposeAddToMentionsAction = {
|
||||
type: COMPOSE_ADD_TO_MENTIONS,
|
||||
id: composeId,
|
||||
account: acct,
|
||||
account: account.acct,
|
||||
};
|
||||
|
||||
return dispatch(action);
|
||||
@@ -821,12 +826,13 @@ interface ComposeRemoveFromMentionsAction {
|
||||
const removeFromMentions = (composeId: string, accountId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const acct = state.accounts.get(accountId)!.acct;
|
||||
const account = selectAccount(state, accountId);
|
||||
if (!account) return;
|
||||
|
||||
const action: ComposeRemoveFromMentionsAction = {
|
||||
type: COMPOSE_REMOVE_FROM_MENTIONS,
|
||||
id: composeId,
|
||||
account: acct,
|
||||
account: account.acct,
|
||||
};
|
||||
|
||||
return dispatch(action);
|
||||
@@ -850,7 +856,7 @@ const eventDiscussionCompose = (composeId: string, status: Status) =>
|
||||
type: COMPOSE_EVENT_REPLY,
|
||||
id: composeId,
|
||||
status: status,
|
||||
account: state.accounts.get(state.me),
|
||||
account: selectOwnAccount(state),
|
||||
explicitAddressing,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { EntityStore } from 'soapbox/entity-store/types';
|
||||
import type { Account } from 'soapbox/schemas';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
|
||||
const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST';
|
||||
@@ -28,11 +31,8 @@ const blockDomain = (domain: string) =>
|
||||
dispatch(blockDomainRequest(domain));
|
||||
|
||||
api(getState).post('/api/v1/domain_blocks', { domain }).then(() => {
|
||||
const at_domain = '@' + domain;
|
||||
const accounts = getState().accounts
|
||||
.filter(item => item.acct.endsWith(at_domain))
|
||||
.map(item => item.id);
|
||||
|
||||
const accounts = selectAccountsByDomain(getState(), domain);
|
||||
if (!accounts) return;
|
||||
dispatch(blockDomainSuccess(domain, accounts));
|
||||
}).catch(err => {
|
||||
dispatch(blockDomainFail(domain, err));
|
||||
@@ -69,8 +69,8 @@ const unblockDomain = (domain: string) =>
|
||||
};
|
||||
|
||||
api(getState).delete('/api/v1/domain_blocks', params).then(() => {
|
||||
const at_domain = '@' + domain;
|
||||
const accounts = getState().accounts.filter(item => item.acct.endsWith(at_domain)).map(item => item.id);
|
||||
const accounts = selectAccountsByDomain(getState(), domain);
|
||||
if (!accounts) return;
|
||||
dispatch(unblockDomainSuccess(domain, accounts));
|
||||
}).catch(err => {
|
||||
dispatch(unblockDomainFail(domain, err));
|
||||
@@ -143,6 +143,15 @@ const expandDomainBlocks = () =>
|
||||
});
|
||||
};
|
||||
|
||||
function selectAccountsByDomain(state: RootState, domain: string): string[] {
|
||||
const store = state.entities[Entities.ACCOUNTS]?.store as EntityStore<Account> | undefined;
|
||||
const entries = store ? Object.entries(store) : undefined;
|
||||
const accounts = entries
|
||||
?.filter(([_, item]) => item && item.acct.endsWith(`@${domain}`))
|
||||
.map(([_, item]) => item!.id);
|
||||
return accounts || [];
|
||||
}
|
||||
|
||||
const expandDomainBlocksRequest = () => ({
|
||||
type: DOMAIN_BLOCKS_EXPAND_REQUEST,
|
||||
});
|
||||
|
||||
@@ -3,16 +3,11 @@ import get from 'lodash/get';
|
||||
|
||||
import KVStore from 'soapbox/storage/kv-store';
|
||||
import { RootState } from 'soapbox/store';
|
||||
import { getAuthUserUrl } from 'soapbox/utils/auth';
|
||||
import { getAuthUserUrl, getMeUrl } from 'soapbox/utils/auth';
|
||||
import { parseVersion } from 'soapbox/utils/features';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
const getMeUrl = (state: RootState) => {
|
||||
const me = state.me;
|
||||
return state.accounts.get(me)?.url;
|
||||
};
|
||||
|
||||
/** Figure out the appropriate instance to fetch depending on the state */
|
||||
export const getHost = (state: RootState) => {
|
||||
const accountUrl = getMeUrl(state) || getAuthUserUrl(state) as string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { selectAccount } from 'soapbox/selectors';
|
||||
import toast from 'soapbox/toast';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
|
||||
@@ -356,7 +357,7 @@ const resetListAdder = () => ({
|
||||
const setupListAdder = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({
|
||||
type: LIST_ADDER_SETUP,
|
||||
account: getState().accounts.get(accountId),
|
||||
account: selectAccount(getState(), accountId),
|
||||
});
|
||||
dispatch(fetchLists());
|
||||
dispatch(fetchAccountLists(accountId));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { selectAccount } from 'soapbox/selectors';
|
||||
import KVStore from 'soapbox/storage/kv-store';
|
||||
import { getAuthUserId, getAuthUserUrl } from 'soapbox/utils/auth';
|
||||
|
||||
@@ -25,7 +26,9 @@ const getMeId = (state: RootState) => state.me || getAuthUserId(state);
|
||||
|
||||
const getMeUrl = (state: RootState) => {
|
||||
const accountId = getMeId(state);
|
||||
return state.accounts.get(accountId)?.url || getAuthUserUrl(state);
|
||||
if (accountId) {
|
||||
return selectAccount(state, accountId)?.url || getAuthUserUrl(state);
|
||||
}
|
||||
};
|
||||
|
||||
const getMeToken = (state: RootState) => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { openModal } from 'soapbox/actions/modals';
|
||||
import OutlineBox from 'soapbox/components/outline-box';
|
||||
import { Stack, Text } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { selectAccount } from 'soapbox/selectors';
|
||||
import toast from 'soapbox/toast';
|
||||
import { isLocal } from 'soapbox/utils/accounts';
|
||||
|
||||
@@ -42,8 +43,8 @@ const messages = defineMessages({
|
||||
const deactivateUserModal = (intl: IntlShape, accountId: string, afterConfirm = () => {}) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const acct = state.accounts.get(accountId)!.acct;
|
||||
const name = state.accounts.get(accountId)!.username;
|
||||
const acct = selectAccount(state, accountId)!.acct;
|
||||
const name = selectAccount(state, accountId)!.username;
|
||||
|
||||
const message = (
|
||||
<Stack space={4}>
|
||||
@@ -75,7 +76,7 @@ const deactivateUserModal = (intl: IntlShape, accountId: string, afterConfirm =
|
||||
const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () => {}) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const account = state.accounts.get(accountId)!;
|
||||
const account = selectAccount(state, accountId)!;
|
||||
const acct = account.acct;
|
||||
const name = account.username;
|
||||
const local = isLocal(account);
|
||||
@@ -115,8 +116,8 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () =
|
||||
const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensitive: boolean, afterConfirm = () => {}) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const accountId = state.statuses.get(statusId)!.account;
|
||||
const acct = state.accounts.get(accountId)!.acct;
|
||||
const accountId = state.statuses.get(statusId)!.account.id;
|
||||
const acct = selectAccount(state, accountId)!.acct;
|
||||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
icon: require('@tabler/icons/alert-triangle.svg'),
|
||||
@@ -136,8 +137,8 @@ const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensiti
|
||||
const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = () => {}) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const accountId = state.statuses.get(statusId)!.account;
|
||||
const acct = state.accounts.get(accountId)!.acct;
|
||||
const accountId = state.statuses.get(statusId)!.account.id;
|
||||
const acct = selectAccount(state, accountId)!.acct;
|
||||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
icon: require('@tabler/icons/trash.svg'),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
@@ -20,7 +20,7 @@ function useAccount(accountId?: string, opts: UseAccountOpts = {}) {
|
||||
const { me } = useLoggedIn();
|
||||
const { withRelationship } = opts;
|
||||
|
||||
const { entity: account, isUnauthorized, ...result } = useEntity<Account>(
|
||||
const { entity, isUnauthorized, ...result } = useEntity<Account>(
|
||||
[Entities.ACCOUNTS, accountId!],
|
||||
() => api.get(`/api/v1/accounts/${accountId}`),
|
||||
{ schema: accountSchema, enabled: !!accountId },
|
||||
@@ -31,8 +31,13 @@ function useAccount(accountId?: string, opts: UseAccountOpts = {}) {
|
||||
isLoading: isRelationshipLoading,
|
||||
} = useRelationship(accountId, { enabled: withRelationship });
|
||||
|
||||
const isBlocked = account?.relationship?.blocked_by === true;
|
||||
const isUnavailable = (me === account?.id) ? false : (isBlocked && !features.blockersVisible);
|
||||
const isBlocked = entity?.relationship?.blocked_by === true;
|
||||
const isUnavailable = (me === entity?.id) ? false : (isBlocked && !features.blockersVisible);
|
||||
|
||||
const account = useMemo(
|
||||
() => entity ? { ...entity, relationship } : undefined,
|
||||
[entity, relationship],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isUnauthorized) {
|
||||
@@ -46,7 +51,7 @@ function useAccount(accountId?: string, opts: UseAccountOpts = {}) {
|
||||
isRelationshipLoading,
|
||||
isUnauthorized,
|
||||
isUnavailable,
|
||||
account: account ? { ...account, relationship } : undefined,
|
||||
account,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
2
app/soapbox/api/hooks/admin/index.ts
Normal file
2
app/soapbox/api/hooks/admin/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { useSuggest } from './useSuggest';
|
||||
export { useVerify } from './useVerify';
|
||||
58
app/soapbox/api/hooks/admin/useSuggest.ts
Normal file
58
app/soapbox/api/hooks/admin/useSuggest.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useTransaction } from 'soapbox/entity-store/hooks';
|
||||
import { EntityCallbacks } from 'soapbox/entity-store/hooks/types';
|
||||
import { useApi, useGetState } from 'soapbox/hooks';
|
||||
import { accountIdsToAccts } from 'soapbox/selectors';
|
||||
|
||||
import type { Account } from 'soapbox/schemas';
|
||||
|
||||
function useSuggest() {
|
||||
const api = useApi();
|
||||
const getState = useGetState();
|
||||
const { transaction } = useTransaction();
|
||||
|
||||
function suggestEffect(accountIds: string[], suggested: boolean) {
|
||||
const updater = (account: Account): Account => {
|
||||
if (account.pleroma) {
|
||||
account.pleroma.is_suggested = suggested;
|
||||
}
|
||||
return account;
|
||||
};
|
||||
|
||||
transaction({
|
||||
Accounts: accountIds.reduce<Record<string, (account: Account) => Account>>(
|
||||
(result, id) => ({ ...result, [id]: updater }),
|
||||
{}),
|
||||
});
|
||||
}
|
||||
|
||||
async function suggest(accountIds: string[], callbacks?: EntityCallbacks<void, unknown>) {
|
||||
const accts = accountIdsToAccts(getState(), accountIds);
|
||||
suggestEffect(accountIds, true);
|
||||
try {
|
||||
await api.patch('/api/v1/pleroma/admin/users/suggest', { nicknames: accts });
|
||||
callbacks?.onSuccess?.();
|
||||
} catch (e) {
|
||||
callbacks?.onError?.(e);
|
||||
suggestEffect(accountIds, false);
|
||||
}
|
||||
}
|
||||
|
||||
async function unsuggest(accountIds: string[], callbacks?: EntityCallbacks<void, unknown>) {
|
||||
const accts = accountIdsToAccts(getState(), accountIds);
|
||||
suggestEffect(accountIds, false);
|
||||
try {
|
||||
await api.patch('/api/v1/pleroma/admin/users/unsuggest', { nicknames: accts });
|
||||
callbacks?.onSuccess?.();
|
||||
} catch (e) {
|
||||
callbacks?.onError?.(e);
|
||||
suggestEffect(accountIds, true);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
suggest,
|
||||
unsuggest,
|
||||
};
|
||||
}
|
||||
|
||||
export { useSuggest };
|
||||
63
app/soapbox/api/hooks/admin/useVerify.ts
Normal file
63
app/soapbox/api/hooks/admin/useVerify.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useTransaction } from 'soapbox/entity-store/hooks';
|
||||
import { EntityCallbacks } from 'soapbox/entity-store/hooks/types';
|
||||
import { useApi, useGetState } from 'soapbox/hooks';
|
||||
import { accountIdsToAccts } from 'soapbox/selectors';
|
||||
|
||||
import type { Account } from 'soapbox/schemas';
|
||||
|
||||
function useVerify() {
|
||||
const api = useApi();
|
||||
const getState = useGetState();
|
||||
const { transaction } = useTransaction();
|
||||
|
||||
function verifyEffect(accountIds: string[], verified: boolean) {
|
||||
const updater = (account: Account): Account => {
|
||||
if (account.pleroma) {
|
||||
const tags = account.pleroma.tags.filter((tag) => tag !== 'verified');
|
||||
if (verified) {
|
||||
tags.push('verified');
|
||||
}
|
||||
account.pleroma.tags = tags;
|
||||
}
|
||||
account.verified = verified;
|
||||
return account;
|
||||
};
|
||||
|
||||
transaction({
|
||||
Accounts: accountIds.reduce<Record<string, (account: Account) => Account>>(
|
||||
(result, id) => ({ ...result, [id]: updater }),
|
||||
{}),
|
||||
});
|
||||
}
|
||||
|
||||
async function verify(accountIds: string[], callbacks?: EntityCallbacks<void, unknown>) {
|
||||
const accts = accountIdsToAccts(getState(), accountIds);
|
||||
verifyEffect(accountIds, true);
|
||||
try {
|
||||
await api.put('/api/v1/pleroma/admin/users/tag', { nicknames: accts, tags: ['verified'] });
|
||||
callbacks?.onSuccess?.();
|
||||
} catch (e) {
|
||||
callbacks?.onError?.(e);
|
||||
verifyEffect(accountIds, false);
|
||||
}
|
||||
}
|
||||
|
||||
async function unverify(accountIds: string[], callbacks?: EntityCallbacks<void, unknown>) {
|
||||
const accts = accountIdsToAccts(getState(), accountIds);
|
||||
verifyEffect(accountIds, false);
|
||||
try {
|
||||
await api.delete('/api/v1/pleroma/admin/users/tag', { data: { nicknames: accts, tags: ['verified'] } });
|
||||
callbacks?.onSuccess?.();
|
||||
} catch (e) {
|
||||
callbacks?.onError?.(e);
|
||||
verifyEffect(accountIds, true);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
verify,
|
||||
unverify,
|
||||
};
|
||||
}
|
||||
|
||||
export { useVerify };
|
||||
@@ -10,6 +10,7 @@ import LinkHeader from 'http-link-header';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import * as BuildConfig from 'soapbox/build-config';
|
||||
import { selectAccount } from 'soapbox/selectors';
|
||||
import { RootState } from 'soapbox/store';
|
||||
import { getAccessToken, getAppToken, isURL, parseBaseURL } from 'soapbox/utils/auth';
|
||||
|
||||
@@ -46,7 +47,7 @@ const maybeParseJSON = (data: string) => {
|
||||
};
|
||||
|
||||
const getAuthBaseURL = createSelector([
|
||||
(state: RootState, me: string | false | null) => state.accounts.getIn([me, 'url']),
|
||||
(state: RootState, me: string | false | null) => me ? selectAccount(state, me)?.url : undefined,
|
||||
(state: RootState, _me: string | false | null) => state.auth.me,
|
||||
], (accountUrl, authUserUrl) => {
|
||||
const baseURL = parseBaseURL(accountUrl) || parseBaseURL(authUserUrl);
|
||||
|
||||
@@ -51,7 +51,7 @@ function useEntity<TEntity extends Entity>(
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEnabled) return;
|
||||
if (!isEnabled || error) return;
|
||||
if (!entity || opts.refetch) {
|
||||
fetchEntity();
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ import { useEffect, useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useAppDispatch, useAppSelector, useLoading } from 'soapbox/hooks';
|
||||
import { type RootState } from 'soapbox/store';
|
||||
|
||||
import { importEntities } from '../actions';
|
||||
import { findEntity } from '../selectors';
|
||||
import { Entity } from '../types';
|
||||
|
||||
import { EntityFn } from './types';
|
||||
@@ -58,16 +58,4 @@ function useEntityLookup<TEntity extends Entity>(
|
||||
};
|
||||
}
|
||||
|
||||
function findEntity<TEntity extends Entity>(
|
||||
state: RootState,
|
||||
entityType: string,
|
||||
lookupFn: LookupFn<TEntity>,
|
||||
) {
|
||||
const cache = state.entities[entityType];
|
||||
|
||||
if (cache) {
|
||||
return (Object.values(cache.store) as TEntity[]).find(lookupFn);
|
||||
}
|
||||
}
|
||||
|
||||
export { useEntityLookup };
|
||||
@@ -44,10 +44,24 @@ function selectEntities<TEntity extends Entity>(state: RootState, path: Entities
|
||||
) : [];
|
||||
}
|
||||
|
||||
/** Find an entity using a finder function. */
|
||||
function findEntity<TEntity extends Entity>(
|
||||
state: RootState,
|
||||
entityType: string,
|
||||
lookupFn: (entity: TEntity) => boolean,
|
||||
) {
|
||||
const cache = state.entities[entityType];
|
||||
|
||||
if (cache) {
|
||||
return (Object.values(cache.store) as TEntity[]).find(lookupFn);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
selectCache,
|
||||
selectList,
|
||||
selectListState,
|
||||
useListState,
|
||||
selectEntities,
|
||||
findEntity,
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import { fetchUsers } from 'soapbox/actions/admin';
|
||||
import { Widget } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { selectAccount } from 'soapbox/selectors';
|
||||
import { compareId } from 'soapbox/utils/comparators';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -24,7 +25,7 @@ const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const accountIds = useAppSelector<ImmutableOrderedSet<string>>((state) => state.admin.get('latestUsers').take(limit));
|
||||
const hasDates = useAppSelector((state) => accountIds.every(id => !!state.accounts.getIn([id, 'created_at'])));
|
||||
const hasDates = useAppSelector((state) => accountIds.every(id => !!selectAccount(state, id)?.created_at));
|
||||
|
||||
const [total, setTotal] = useState(accountIds.size);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
|
||||
import { FormattedList, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { useAppDispatch, useAppSelector, useCompose, useFeatures } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector, useCompose, useFeatures, useOwnAccount } from 'soapbox/hooks';
|
||||
import { statusToMentionsAccountIdsArray } from 'soapbox/reducers/compose';
|
||||
import { makeGetStatus } from 'soapbox/selectors';
|
||||
import { isPubkey } from 'soapbox/utils/nostr';
|
||||
@@ -21,7 +21,7 @@ const ReplyMentions: React.FC<IReplyMentions> = ({ composeId }) => {
|
||||
const getStatus = useCallback(makeGetStatus(), []);
|
||||
const status = useAppSelector<StatusEntity | null>(state => getStatus(state, { id: compose.in_reply_to! }));
|
||||
const to = compose.to;
|
||||
const account = useAppSelector((state) => state.accounts.get(state.me));
|
||||
const { account } = useOwnAccount();
|
||||
|
||||
if (!features.explicitAddressing || !status || !to) {
|
||||
return null;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { expandSearch, setFilter, setSearchAccount } from 'soapbox/actions/search';
|
||||
import { fetchTrendingStatuses } from 'soapbox/actions/trending-statuses';
|
||||
import { useAccount } from 'soapbox/api/hooks';
|
||||
import Hashtag from 'soapbox/components/hashtag';
|
||||
import IconButton from 'soapbox/components/icon-button';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
@@ -38,8 +39,8 @@ const SearchResults = () => {
|
||||
const trends = useAppSelector((state) => state.trends.items);
|
||||
const submitted = useAppSelector((state) => state.search.submitted);
|
||||
const selectedFilter = useAppSelector((state) => state.search.filter);
|
||||
const filterByAccount = useAppSelector((state) => state.search.accountId);
|
||||
const account = useAppSelector((state) => state.accounts.get(filterByAccount)?.acct);
|
||||
const filterByAccount = useAppSelector((state) => state.search.accountId || undefined);
|
||||
const { account } = useAccount(filterByAccount);
|
||||
|
||||
const handleLoadMore = () => dispatch(expandSearch(selectedFilter));
|
||||
|
||||
@@ -205,7 +206,7 @@ const SearchResults = () => {
|
||||
<FormattedMessage
|
||||
id='search_results.filter_message'
|
||||
defaultMessage='You are searching for posts from @{acct}.'
|
||||
values={{ acct: <strong className='break-words'>{account}</strong> }}
|
||||
values={{ acct: <strong className='break-words'>{account?.acct}</strong> }}
|
||||
/>
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
@@ -3,10 +3,11 @@ import { FormattedMessage } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useAppSelector, useCompose } from 'soapbox/hooks';
|
||||
import { selectOwnAccount } from 'soapbox/selectors';
|
||||
|
||||
import Warning from '../components/warning';
|
||||
|
||||
const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\w*[a-zA-Z·]\w*)/i;
|
||||
const APPROX_HASHTAG_RE = /(?:^|[^/)\w])#(\w*[a-zA-Z·]\w*)/i;
|
||||
|
||||
interface IWarningWrapper {
|
||||
composeId: string
|
||||
@@ -15,25 +16,50 @@ interface IWarningWrapper {
|
||||
const WarningWrapper: React.FC<IWarningWrapper> = ({ composeId }) => {
|
||||
const compose = useCompose(composeId);
|
||||
|
||||
const me = useAppSelector((state) => state.me);
|
||||
|
||||
const needsLockWarning = useAppSelector((state) => compose.privacy === 'private' && !state.accounts.get(me)!.locked);
|
||||
const needsLockWarning = useAppSelector((state) => compose.privacy === 'private' && !selectOwnAccount(state)!.locked);
|
||||
const hashtagWarning = (compose.privacy !== 'public' && compose.privacy !== 'group') && APPROX_HASHTAG_RE.test(compose.text);
|
||||
const directMessageWarning = compose.privacy === 'direct';
|
||||
|
||||
if (needsLockWarning) {
|
||||
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <Link to='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></Link> }} />} />;
|
||||
return (
|
||||
<Warning
|
||||
message={(
|
||||
<FormattedMessage
|
||||
id='compose_form.lock_disclaimer'
|
||||
defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.'
|
||||
values={{
|
||||
locked: (
|
||||
<Link to='/settings/profile'>
|
||||
<FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' />
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (hashtagWarning) {
|
||||
return <Warning message={<FormattedMessage id='compose_form.hashtag_warning' defaultMessage="This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag." />} />;
|
||||
return (
|
||||
<Warning
|
||||
message={(
|
||||
<FormattedMessage
|
||||
id='compose_form.hashtag_warning'
|
||||
defaultMessage="This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag."
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (directMessageWarning) {
|
||||
const message = (
|
||||
<span>
|
||||
<FormattedMessage id='compose_form.direct_message_warning' defaultMessage='This post will only be sent to the mentioned users.' />
|
||||
{/* <a href='/about/tos' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a> */}
|
||||
<FormattedMessage
|
||||
id='compose_form.direct_message_warning'
|
||||
defaultMessage='This post will only be sent to the mentioned users.'
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useHistory } from 'react-router-dom';
|
||||
import { markConversationRead } from 'soapbox/actions/conversations';
|
||||
import StatusContainer from 'soapbox/containers/status-container';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { selectAccount } from 'soapbox/selectors';
|
||||
|
||||
interface IConversation {
|
||||
conversationId: string
|
||||
@@ -19,7 +20,7 @@ const Conversation: React.FC<IConversation> = ({ conversationId, onMoveUp, onMov
|
||||
const conversation = state.conversations.items.find(x => x.id === conversationId)!;
|
||||
|
||||
return {
|
||||
accounts: conversation.accounts.map((accountId: string) => state.accounts.get(accountId)!),
|
||||
accounts: conversation.accounts.map((accountId: string) => selectAccount(state, accountId)!),
|
||||
unread: conversation.unread,
|
||||
lastStatusId: conversation.last_status || null,
|
||||
};
|
||||
|
||||
@@ -157,7 +157,7 @@ describe('<FeedCarousel />', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('prev-page')).not.toHaveAttribute('disabled');
|
||||
expect(screen.getByTestId('next-page')).toHaveAttribute('disabled');
|
||||
// expect(screen.getByTestId('next-page')).toHaveAttribute('disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import MissingIndicator from 'soapbox/components/missing-indicator';
|
||||
import StatusList from 'soapbox/components/status-list';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { selectOwnAccount } from 'soapbox/selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.pins', defaultMessage: 'Pinned posts' },
|
||||
@@ -17,7 +18,7 @@ const PinnedStatuses = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { username } = useParams<{ username: string }>();
|
||||
|
||||
const meUsername = useAppSelector((state) => state.accounts.get(state.me)?.username || '');
|
||||
const meUsername = useAppSelector((state) => selectOwnAccount(state)?.username || '');
|
||||
const statusIds = useAppSelector((state) => state.status_lists.get('pins')!.items);
|
||||
const isLoading = useAppSelector((state) => !!state.status_lists.get('pins')!.isLoading);
|
||||
const hasMore = useAppSelector((state) => !!state.status_lists.get('pins')!.next);
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { buildAccount } from 'soapbox/jest/factory';
|
||||
|
||||
import { render, screen, waitFor } from '../../../jest/test-helpers';
|
||||
import { normalizeInstance } from '../../../normalizers';
|
||||
import UI from '../index';
|
||||
import { WrappedRoute } from '../util/react-router-helpers';
|
||||
|
||||
const TestableComponent = () => (
|
||||
<Switch>
|
||||
<Route path='/@:username/posts/:statusId' exact><UI /></Route>
|
||||
<Route path='/@:username/media' exact><UI /></Route>
|
||||
<Route path='/@:username' exact><UI /></Route>
|
||||
<Route path='/login' exact><span data-testid='sign-in'>Sign in</span></Route>
|
||||
|
||||
{/* WrappedRount will redirect to /login for logged out users... which will resolve to the route above! */}
|
||||
<WrappedRoute path='/notifications' component={() => null} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
describe('<UI />', () => {
|
||||
let store: any;
|
||||
|
||||
beforeEach(() => {
|
||||
store = {
|
||||
me: false,
|
||||
accounts: {
|
||||
'1': buildAccount({
|
||||
id: '1',
|
||||
acct: 'username',
|
||||
display_name: 'My name',
|
||||
avatar: 'test.jpg',
|
||||
}),
|
||||
},
|
||||
instance: normalizeInstance({ registrations: true }),
|
||||
};
|
||||
});
|
||||
|
||||
describe('when logged out', () => {
|
||||
describe('when viewing a Profile Page', () => {
|
||||
it('should render the Profile page', async() => {
|
||||
render(
|
||||
<TestableComponent />,
|
||||
{},
|
||||
store,
|
||||
{ initialEntries: ['/@username'] },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('cta-banner')).toHaveTextContent('Sign up now to discuss');
|
||||
}, {
|
||||
timeout: 5000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when viewing a Status Page', () => {
|
||||
it('should render the Status page', async() => {
|
||||
render(
|
||||
<TestableComponent />,
|
||||
{},
|
||||
store,
|
||||
{ initialEntries: ['/@username/posts/12'] },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('cta-banner')).toHaveTextContent('Sign up now to discuss');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when viewing Notifications', () => {
|
||||
it('should redirect to the login page', async() => {
|
||||
render(
|
||||
<TestableComponent />,
|
||||
{},
|
||||
store,
|
||||
{ initialEntries: ['/notifications'] },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('sign-in')).toHaveTextContent('Sign in');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,10 @@
|
||||
import React, { ChangeEventHandler, useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import {
|
||||
verifyUser,
|
||||
unverifyUser,
|
||||
suggestUsers,
|
||||
unsuggestUsers,
|
||||
setBadges as saveBadges,
|
||||
} from 'soapbox/actions/admin';
|
||||
import { setBadges as saveBadges } from 'soapbox/actions/admin';
|
||||
import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
|
||||
import { useAccount } from 'soapbox/api/hooks';
|
||||
import { useSuggest, useVerify } from 'soapbox/api/hooks/admin';
|
||||
import Account from 'soapbox/components/account';
|
||||
import List, { ListItem } from 'soapbox/components/list';
|
||||
import MissingIndicator from 'soapbox/components/missing-indicator';
|
||||
@@ -45,6 +40,8 @@ const AccountModerationModal: React.FC<IAccountModerationModal> = ({ onClose, ac
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { suggest, unsuggest } = useSuggest();
|
||||
const { verify, unverify } = useVerify();
|
||||
const { account: ownAccount } = useOwnAccount();
|
||||
const features = useFeatures();
|
||||
const { account } = useAccount(accountId);
|
||||
@@ -70,22 +67,22 @@ const AccountModerationModal: React.FC<IAccountModerationModal> = ({ onClose, ac
|
||||
const { checked } = e.target;
|
||||
|
||||
const message = checked ? messages.userVerified : messages.userUnverified;
|
||||
const action = checked ? verifyUser : unverifyUser;
|
||||
const action = checked ? verify : unverify;
|
||||
|
||||
dispatch(action(account.id))
|
||||
.then(() => toast.success(intl.formatMessage(message, { acct: account.acct })))
|
||||
.catch(() => {});
|
||||
action([account.id], {
|
||||
onSuccess: () => toast.success(intl.formatMessage(message, { acct: account.acct })),
|
||||
});
|
||||
};
|
||||
|
||||
const handleSuggestedChange: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
const { checked } = e.target;
|
||||
|
||||
const message = checked ? messages.userSuggested : messages.userUnsuggested;
|
||||
const action = checked ? suggestUsers : unsuggestUsers;
|
||||
const action = checked ? suggest : unsuggest;
|
||||
|
||||
dispatch(action([account.id]))
|
||||
.then(() => toast.success(intl.formatMessage(message, { acct: account.acct })))
|
||||
.catch(() => {});
|
||||
action([account.id], {
|
||||
onSuccess: () => toast.success(intl.formatMessage(message, { acct: account.acct })),
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeactivate = () => {
|
||||
|
||||
@@ -110,14 +110,8 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
|
||||
const handleStatusClick: React.MouseEventHandler = e => {
|
||||
if (status && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
|
||||
dispatch((_, getState) => {
|
||||
const account = typeof status.account === 'string' ? getState().accounts.get(status.account) : status.account;
|
||||
if (!account) return;
|
||||
|
||||
history.push(`/@${account.acct}/posts/${status?.id}`);
|
||||
onClose();
|
||||
});
|
||||
history.push(`/@${status.account.acct}/posts/${status?.id}`);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Modal } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useCompose } from 'soapbox/hooks';
|
||||
import { useAppSelector, useCompose, useOwnAccount } from 'soapbox/hooks';
|
||||
import { statusToMentionsAccountIdsArray } from 'soapbox/reducers/compose';
|
||||
import { makeGetStatus } from 'soapbox/selectors';
|
||||
|
||||
@@ -20,7 +20,7 @@ const ReplyMentionsModal: React.FC<IReplyMentionsModal> = ({ composeId, onClose
|
||||
|
||||
const getStatus = useCallback(makeGetStatus(), []);
|
||||
const status = useAppSelector<StatusEntity | null>(state => getStatus(state, { id: compose.in_reply_to! }));
|
||||
const account = useAppSelector((state) => state.accounts.get(state.me));
|
||||
const { account } = useOwnAccount();
|
||||
|
||||
const mentions = statusToMentionsAccountIdsArray(status!, account!);
|
||||
const author = (status?.account as AccountEntity).id;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useHistory } from 'react-router-dom';
|
||||
import { remoteInteraction } from 'soapbox/actions/interactions';
|
||||
import { Button, Form, Input, Modal, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useAppDispatch, useFeatures, useInstance, useRegistrationStatus } from 'soapbox/hooks';
|
||||
import { selectAccount } from 'soapbox/selectors';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -32,7 +33,7 @@ const UnauthorizedModal: React.FC<IUnauthorizedModal> = ({ action, onClose, acco
|
||||
const instance = useInstance();
|
||||
const { isOpen } = useRegistrationStatus();
|
||||
|
||||
const username = useAppSelector(state => state.accounts.get(accountId)?.display_name);
|
||||
const username = useAppSelector(state => selectAccount(state, accountId!)?.display_name);
|
||||
const features = useFeatures();
|
||||
|
||||
const [account, setAccount] = useState('');
|
||||
|
||||
@@ -26,7 +26,7 @@ const importMessagesWithCustom = (locale: string): Promise<MessageJson> => {
|
||||
});
|
||||
};
|
||||
|
||||
const locales: string[] = [
|
||||
const locales = [
|
||||
'ar',
|
||||
'ast',
|
||||
'bg',
|
||||
@@ -91,7 +91,7 @@ const locales: string[] = [
|
||||
'zh-CN',
|
||||
'zh-HK',
|
||||
'zh-TW',
|
||||
];
|
||||
] as const;
|
||||
|
||||
/** Soapbox locales map */
|
||||
const messages = locales.reduce((acc, locale) => {
|
||||
@@ -100,3 +100,4 @@ const messages = locales.reduce((acc, locale) => {
|
||||
}, {} as Record<string, () => Promise<MessageJson>>);
|
||||
|
||||
export default messages;
|
||||
export { locales };
|
||||
@@ -1,33 +0,0 @@
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { ThunkMiddleware } from 'redux-thunk';
|
||||
|
||||
/** Whether the action is considered a failure. */
|
||||
const isFailType = (type: string): boolean => type.endsWith('_FAIL');
|
||||
|
||||
/** Whether the action is a failure to fetch from browser storage. */
|
||||
const isRememberFailType = (type: string): boolean => type.endsWith('_REMEMBER_FAIL');
|
||||
|
||||
/** Whether the error contains an Axios response. */
|
||||
const hasResponse = (error: any): boolean => Boolean(error && error.response);
|
||||
|
||||
/** Don't show 401's. */
|
||||
const authorized = (error: any): boolean => error?.response?.status !== 401;
|
||||
|
||||
/** Whether the error should be shown to the user. */
|
||||
const shouldShowError = ({ type, skipAlert, error }: AnyAction): boolean => {
|
||||
return !skipAlert && hasResponse(error) && authorized(error) && isFailType(type) && !isRememberFailType(type);
|
||||
};
|
||||
|
||||
/** Middleware to display Redux errors to the user. */
|
||||
const errorsMiddleware = (): ThunkMiddleware =>
|
||||
() => next => action => {
|
||||
if (shouldShowError(action)) {
|
||||
toast.showAlertForError(action.error);
|
||||
}
|
||||
|
||||
return next(action);
|
||||
};
|
||||
|
||||
export default errorsMiddleware;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Map as ImmutableMap, Record as ImmutableRecord } from 'immutable';
|
||||
|
||||
import { ACCOUNT_IMPORT } from 'soapbox/actions/importer';
|
||||
|
||||
import reducer from '../accounts';
|
||||
|
||||
describe('accounts reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {} as any)).toEqual(ImmutableMap());
|
||||
});
|
||||
|
||||
describe('ACCOUNT_IMPORT', () => {
|
||||
it('parses the account as a Record', () => {
|
||||
const account = require('soapbox/__fixtures__/pleroma-account.json');
|
||||
const action = { type: ACCOUNT_IMPORT, account };
|
||||
const result = reducer(undefined, action).get('9v5bmRalQvjOy0ECcC');
|
||||
|
||||
expect(ImmutableRecord.isRecord(result)).toBe(true);
|
||||
});
|
||||
|
||||
it('minifies a moved account', () => {
|
||||
const account = require('soapbox/__fixtures__/account-moved.json');
|
||||
const action = { type: ACCOUNT_IMPORT, account };
|
||||
const result = reducer(undefined, action).get('106801667066418367');
|
||||
|
||||
expect(result?.moved).toBe('107945464165013501');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import {
|
||||
AUTH_APP_CREATED,
|
||||
@@ -300,7 +300,7 @@ describe('auth reducer', () => {
|
||||
it('sets the value of `me`', () => {
|
||||
const action = {
|
||||
type: SWITCH_ACCOUNT,
|
||||
account: fromJS({ url: 'https://gleasonator.com/users/benis' }),
|
||||
account: { url: 'https://gleasonator.com/users/benis' },
|
||||
};
|
||||
|
||||
const result = reducer(undefined, action);
|
||||
|
||||
@@ -6,7 +6,6 @@ describe('root reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
const result = reducer(undefined, {} as any);
|
||||
expect(ImmutableRecord.isRecord(result)).toBe(true);
|
||||
expect(result.accounts.get('')).toBe(undefined);
|
||||
expect(result.instance.version).toEqual('0.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,10 +23,6 @@ import {
|
||||
ADMIN_USERS_DELETE_FAIL,
|
||||
ADMIN_USERS_DEACTIVATE_REQUEST,
|
||||
ADMIN_USERS_DEACTIVATE_FAIL,
|
||||
ADMIN_USERS_SUGGEST_REQUEST,
|
||||
ADMIN_USERS_SUGGEST_FAIL,
|
||||
ADMIN_USERS_UNSUGGEST_REQUEST,
|
||||
ADMIN_USERS_UNSUGGEST_FAIL,
|
||||
} from 'soapbox/actions/admin';
|
||||
import { CHATS_FETCH_SUCCESS, CHATS_EXPAND_SUCCESS, CHAT_FETCH_SUCCESS } from 'soapbox/actions/chats';
|
||||
import {
|
||||
@@ -234,14 +230,6 @@ const importAdminUsers = (state: State, adminUsers: Array<Record<string, any>>):
|
||||
});
|
||||
};
|
||||
|
||||
const setSuggested = (state: State, accountIds: Array<string>, isSuggested: boolean): State => {
|
||||
return state.withMutations(state => {
|
||||
accountIds.forEach(id => {
|
||||
state.setIn([id, 'pleroma', 'is_suggested'], isSuggested);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default function accounts(state: State = initialState, action: AnyAction): State {
|
||||
switch (action.type) {
|
||||
case ACCOUNT_IMPORT:
|
||||
@@ -280,12 +268,6 @@ export default function accounts(state: State = initialState, action: AnyAction)
|
||||
return setActive(state, action.accountIds, true);
|
||||
case ADMIN_USERS_FETCH_SUCCESS:
|
||||
return importAdminUsers(state, action.users);
|
||||
case ADMIN_USERS_SUGGEST_REQUEST:
|
||||
case ADMIN_USERS_UNSUGGEST_FAIL:
|
||||
return setSuggested(state, action.accountIds, true);
|
||||
case ADMIN_USERS_UNSUGGEST_REQUEST:
|
||||
case ADMIN_USERS_SUGGEST_FAIL:
|
||||
return setSuggested(state, action.accountIds, false);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ const reducer = (state: State, action: AnyAction) => {
|
||||
case VERIFY_CREDENTIALS_FAIL:
|
||||
return deleteForbiddenToken(state, action.error, action.token);
|
||||
case SWITCH_ACCOUNT:
|
||||
return state.set('me', action.account.get('url'));
|
||||
return state.set('me', action.account.url);
|
||||
case ME_FETCH_SKIP:
|
||||
return state.set('me', null);
|
||||
case MASTODON_PRELOAD_IMPORT:
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { Record as ImmutableRecord } from 'immutable';
|
||||
import { combineReducers } from 'redux-immutable';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { AUTH_LOGGED_OUT } from 'soapbox/actions/auth';
|
||||
import * as BuildConfig from 'soapbox/build-config';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import entities from 'soapbox/entity-store/reducer';
|
||||
import { immutableizeStore, type LegacyStore } from 'soapbox/utils/legacy';
|
||||
|
||||
import account_notes from './account-notes';
|
||||
import accounts_meta from './accounts-meta';
|
||||
@@ -70,12 +67,7 @@ import trends from './trends';
|
||||
import user_lists from './user-lists';
|
||||
import verification from './verification';
|
||||
|
||||
import type { AnyAction, Reducer } from 'redux';
|
||||
import type { EntityStore } from 'soapbox/entity-store/types';
|
||||
import type { Account } from 'soapbox/schemas';
|
||||
|
||||
const reducers = {
|
||||
accounts: ((state: any = {}) => state) as (state: any) => EntityStore<Account> & LegacyStore<Account>,
|
||||
account_notes,
|
||||
accounts_meta,
|
||||
admin,
|
||||
@@ -175,19 +167,4 @@ const rootReducer: typeof appReducer = (state, action) => {
|
||||
}
|
||||
};
|
||||
|
||||
type InferState<R> = R extends Reducer<infer S> ? S : never;
|
||||
|
||||
const accountsSelector = createSelector(
|
||||
(state: InferState<typeof appReducer>) => state.entities[Entities.ACCOUNTS]?.store as EntityStore<Account> || {},
|
||||
(accounts) => immutableizeStore<Account, EntityStore<Account>>(accounts),
|
||||
);
|
||||
|
||||
const extendedRootReducer = (
|
||||
state: InferState<typeof appReducer>,
|
||||
action: AnyAction,
|
||||
): ReturnType<typeof rootReducer> => {
|
||||
const extendedState = rootReducer(state, action);
|
||||
return extendedState.set('accounts', accountsSelector(extendedState));
|
||||
};
|
||||
|
||||
export default extendedRootReducer as Reducer<ReturnType<typeof extendedRootReducer>>;
|
||||
export default rootReducer;
|
||||
36
app/soapbox/schemas/soapbox/settings.ts
Normal file
36
app/soapbox/schemas/soapbox/settings.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { locales } from 'soapbox/locales/messages';
|
||||
|
||||
const skinToneSchema = z.union([
|
||||
z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5), z.literal(6),
|
||||
]);
|
||||
|
||||
const settingsSchema = z.object({
|
||||
onboarded: z.boolean().catch(false),
|
||||
skinTone: skinToneSchema.catch(1),
|
||||
reduceMotion: z.boolean().catch(false),
|
||||
underlineLinks: z.boolean().catch(false),
|
||||
autoPlayGif: z.boolean().catch(true),
|
||||
displayMedia: z.enum(['default', 'hide_all', 'show_all']).catch('default'),
|
||||
expandSpoilers: z.boolean().catch(false),
|
||||
unfollowModal: z.boolean().catch(false),
|
||||
boostModal: z.boolean().catch(false),
|
||||
deleteModal: z.boolean().catch(true),
|
||||
missingDescriptionModal: z.boolean().catch(false),
|
||||
defaultPrivacy: z.enum(['public', 'unlisted', 'private', 'direct']).catch('public'),
|
||||
defaultContentType: z.enum(['text/plain', 'text/markdown']).catch('text/plain'),
|
||||
themeMode: z.enum(['system', 'light', 'dark']).catch('system'),
|
||||
locale: z.string().catch(navigator.language).pipe(z.enum(locales)).catch('en'),
|
||||
showExplanationBox: z.boolean().catch(true),
|
||||
explanationBox: z.boolean().catch(true),
|
||||
autoloadTimelines: z.boolean().catch(true),
|
||||
autoloadMore: z.boolean().catch(true),
|
||||
systemFont: z.boolean().catch(false),
|
||||
demetricator: z.boolean().catch(false),
|
||||
isDeveloper: z.boolean().catch(false),
|
||||
});
|
||||
|
||||
type Settings = z.infer<typeof settingsSchema>;
|
||||
|
||||
export { settingsSchema, type Settings };
|
||||
@@ -14,13 +14,27 @@ import ConfigDB from 'soapbox/utils/config-db';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import { shouldFilter } from 'soapbox/utils/timelines';
|
||||
|
||||
import type { EntityStore } from 'soapbox/entity-store/types';
|
||||
import type { ContextType } from 'soapbox/normalizers/filter';
|
||||
import type { ReducerChat } from 'soapbox/reducers/chats';
|
||||
import type { Account as AccountSchema } from 'soapbox/schemas';
|
||||
import type { RootState } from 'soapbox/store';
|
||||
import type { Account, Filter as FilterEntity, Notification, Status } from 'soapbox/types/entities';
|
||||
|
||||
const normalizeId = (id: any): string => typeof id === 'string' ? id : '';
|
||||
|
||||
export function selectAccount(state: RootState, accountId: string) {
|
||||
return state.entities[Entities.ACCOUNTS]?.store[accountId] as AccountSchema | undefined;
|
||||
}
|
||||
|
||||
export function selectOwnAccount(state: RootState) {
|
||||
if (state.me) {
|
||||
return selectAccount(state, state.me);
|
||||
}
|
||||
}
|
||||
|
||||
export const accountIdsToAccts = (state: RootState, ids: string[]) => ids.map((id) => selectAccount(state, id)!.acct);
|
||||
|
||||
const getAccountBase = (state: RootState, id: string) => state.entities[Entities.ACCOUNTS]?.store[id] as Account | undefined;
|
||||
const getAccountRelationship = (state: RootState, id: string) => state.relationships.get(id);
|
||||
|
||||
@@ -144,8 +158,8 @@ export const makeGetStatus = () => {
|
||||
export const makeGetNotification = () => {
|
||||
return createSelector([
|
||||
(_state: RootState, notification: Notification) => notification,
|
||||
(state: RootState, notification: Notification) => state.accounts.get(normalizeId(notification.account)),
|
||||
(state: RootState, notification: Notification) => state.accounts.get(normalizeId(notification.target)),
|
||||
(state: RootState, notification: Notification) => selectAccount(state, normalizeId(notification.account)),
|
||||
(state: RootState, notification: Notification) => selectAccount(state, normalizeId(notification.target)),
|
||||
(state: RootState, notification: Notification) => state.statuses.get(normalizeId(notification.status)),
|
||||
], (notification, account, target, status) => {
|
||||
return notification.merge({
|
||||
@@ -193,7 +207,7 @@ export const makeGetChat = () => {
|
||||
return createSelector(
|
||||
[
|
||||
(state: RootState, { id }: APIChat) => state.chats.items.get(id) as ReducerChat,
|
||||
(state: RootState, { id }: APIChat) => state.accounts.get(state.chats.items.getIn([id, 'account'])),
|
||||
(state: RootState, { id }: APIChat) => selectAccount(state, state.chats.items.getIn([id, 'account']) as string),
|
||||
(state: RootState, { last_message }: APIChat) => state.chat_messages.get(last_message),
|
||||
],
|
||||
|
||||
@@ -216,10 +230,8 @@ export const makeGetReport = () => {
|
||||
return createSelector(
|
||||
[
|
||||
(state: RootState, id: string) => state.admin.reports.get(id),
|
||||
(state: RootState, id: string) => state.accounts.get(state.admin.reports.get(id)?.account || ''),
|
||||
(state: RootState, id: string) => state.accounts.get(state.admin.reports.get(id)?.target_account || ''),
|
||||
// (state: RootState, id: string) => state.accounts.get(state.admin.reports.get(id)?.action_taken_by_account || ''),
|
||||
// (state: RootState, id: string) => state.accounts.get(state.admin.reports.get(id)?.assigned_account || ''),
|
||||
(state: RootState, id: string) => selectAccount(state, state.admin.reports.get(id)?.account || ''),
|
||||
(state: RootState, id: string) => selectAccount(state, state.admin.reports.get(id)?.target_account || ''),
|
||||
(state: RootState, id: string) => ImmutableList(fromJS(state.admin.reports.get(id)?.statuses)).map(
|
||||
statusId => state.statuses.get(normalizeId(statusId)))
|
||||
.filter((s: any) => s)
|
||||
@@ -255,7 +267,7 @@ const getAuthUserIds = createSelector([
|
||||
|
||||
export const makeGetOtherAccounts = () => {
|
||||
return createSelector([
|
||||
(state: RootState) => state.accounts,
|
||||
(state: RootState) => state.entities[Entities.ACCOUNTS]?.store as EntityStore<AccountSchema>,
|
||||
getAuthUserIds,
|
||||
(state: RootState) => state.me,
|
||||
],
|
||||
@@ -263,7 +275,7 @@ export const makeGetOtherAccounts = () => {
|
||||
return authUserIds
|
||||
.reduce((list: ImmutableList<any>, id: string) => {
|
||||
if (id === me) return list;
|
||||
const account = accounts.get(id);
|
||||
const account = accounts[id];
|
||||
return account ? list.push(account) : list;
|
||||
}, ImmutableList());
|
||||
});
|
||||
@@ -276,10 +288,11 @@ const getSimplePolicy = createSelector([
|
||||
return instancePolicy.merge(ConfigDB.toSimplePolicy(configs));
|
||||
});
|
||||
|
||||
const getRemoteInstanceFavicon = (state: RootState, host: string) => (
|
||||
(state.accounts.find(account => getDomain(account) === host) || ImmutableMap())
|
||||
.getIn(['pleroma', 'favicon'])
|
||||
);
|
||||
const getRemoteInstanceFavicon = (state: RootState, host: string) => {
|
||||
const accounts = state.entities[Entities.ACCOUNTS]?.store as EntityStore<AccountSchema>;
|
||||
const account = Object.entries(accounts).find(([_, account]) => account && getDomain(account) === host)?.[1];
|
||||
return account?.pleroma?.favicon;
|
||||
};
|
||||
|
||||
const getRemoteInstanceFederation = (state: RootState, host: string) => (
|
||||
getSimplePolicy(state)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import thunk, { ThunkDispatch } from 'redux-thunk';
|
||||
|
||||
import errorsMiddleware from './middleware/errors';
|
||||
import soundsMiddleware from './middleware/sounds';
|
||||
import appReducer from './reducers';
|
||||
|
||||
@@ -11,7 +10,6 @@ export const store = configureStore({
|
||||
reducer: appReducer,
|
||||
middleware: [
|
||||
thunk,
|
||||
errorsMiddleware(),
|
||||
soundsMiddleware(),
|
||||
],
|
||||
devTools: true,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
import { selectAccount, selectOwnAccount } from 'soapbox/selectors';
|
||||
|
||||
import type { RootState } from 'soapbox/store';
|
||||
|
||||
export const validId = (id: any) => typeof id === 'string' && id !== 'null' && id !== 'undefined';
|
||||
@@ -22,10 +24,7 @@ export const parseBaseURL = (url: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getLoggedInAccount = (state: RootState) => {
|
||||
const me = state.me;
|
||||
return state.accounts.get(me);
|
||||
};
|
||||
export const getLoggedInAccount = (state: RootState) => selectOwnAccount(state);
|
||||
|
||||
export const isLoggedIn = (getState: () => RootState) => {
|
||||
return validId(getState().me);
|
||||
@@ -35,7 +34,7 @@ export const getAppToken = (state: RootState) => state.auth.app.access_token as
|
||||
|
||||
export const getUserToken = (state: RootState, accountId?: string | false | null) => {
|
||||
if (!accountId) return;
|
||||
const accountUrl = state.accounts[accountId]?.url;
|
||||
const accountUrl = selectAccount(state, accountId)?.url;
|
||||
if (!accountUrl) return;
|
||||
return state.auth.users.get(accountUrl)?.access_token;
|
||||
};
|
||||
@@ -66,3 +65,5 @@ export const getAuthUserUrl = (state: RootState) => {
|
||||
/** Get the VAPID public key. */
|
||||
export const getVapidKey = (state: RootState) =>
|
||||
(state.auth.app.vapid_key || state.instance.pleroma.get('vapid_public_key')) as string;
|
||||
|
||||
export const getMeUrl = (state: RootState) => selectOwnAccount(state)?.url;
|
||||
@@ -6,6 +6,7 @@
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import * as BuildConfig from 'soapbox/build-config';
|
||||
import { isPrerendered } from 'soapbox/precheck';
|
||||
import { selectOwnAccount } from 'soapbox/selectors';
|
||||
import { isURL } from 'soapbox/utils/auth';
|
||||
|
||||
import type { RootState } from 'soapbox/store';
|
||||
@@ -39,6 +40,6 @@ const getHost = (url: any): string => {
|
||||
|
||||
/** Get the baseURL of the instance. */
|
||||
export const getBaseURL = (state: RootState): string => {
|
||||
const account = state.accounts.get(state.me);
|
||||
const account = selectOwnAccount(state);
|
||||
return isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : getHost(account?.url);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user