Merge remote-tracking branch 'soapbox/develop' into lexical
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
|
@ -1,43 +0,0 @@
|
|||
import sharedConfig from '../webpack/shared';
|
||||
|
||||
import type { StorybookConfig } from '@storybook/core-common';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: [
|
||||
'../stories/**/*.stories.mdx',
|
||||
'../stories/**/*.stories.@(js|jsx|ts|tsx)'
|
||||
],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'storybook-react-intl',
|
||||
{
|
||||
name: '@storybook/addon-postcss',
|
||||
options: {
|
||||
postcssLoaderOptions: {
|
||||
implementation: require('postcss'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
framework: '@storybook/react',
|
||||
core: {
|
||||
builder: '@storybook/builder-webpack5',
|
||||
},
|
||||
webpackFinal: async (config) => {
|
||||
config.resolve!.alias = {
|
||||
...sharedConfig.resolve!.alias,
|
||||
...config.resolve!.alias,
|
||||
};
|
||||
|
||||
config.resolve!.modules = [
|
||||
...sharedConfig.resolve!.modules!,
|
||||
...config.resolve!.modules!,
|
||||
];
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -1,22 +0,0 @@
|
|||
import '../app/styles/tailwind.css';
|
||||
import '../stories/theme.css';
|
||||
|
||||
import { addDecorator, Story } from '@storybook/react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import React from 'react';
|
||||
|
||||
const withProvider = (Story: Story) => (
|
||||
<IntlProvider locale='en'><Story /></IntlProvider>
|
||||
);
|
||||
|
||||
addDecorator(withProvider);
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- UI: unified design of "approve" and "reject" buttons in follow requests and waitlist.
|
||||
- UI: added sticky column header.
|
||||
- UI: add specific zones the user can drag-and-drop files.
|
||||
- UI: disable toast notifications for API errors.
|
||||
|
||||
### Fixed
|
||||
- Posts: fixed emojis being cut off in reactions modal.
|
||||
|
|
|
@ -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}`));
|
||||
|
|
|
@ -7,6 +7,7 @@ import api from 'soapbox/api';
|
|||
import { isNativeEmoji } from 'soapbox/features/emoji';
|
||||
import emojiSearch from 'soapbox/features/emoji/search';
|
||||
import { normalizeTag } from 'soapbox/normalizers';
|
||||
import { selectAccount, selectOwnAccount } from 'soapbox/selectors';
|
||||
import { tagHistory } from 'soapbox/settings';
|
||||
import toast from 'soapbox/toast';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
|
@ -150,12 +151,14 @@ const replyCompose = (status: Status) =>
|
|||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const { explicitAddressing } = getFeatures(instance);
|
||||
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,
|
||||
};
|
||||
|
||||
|
@ -186,7 +189,7 @@ const quoteCompose = (status: Status) =>
|
|||
type: COMPOSE_QUOTE,
|
||||
id: 'compose-modal',
|
||||
status: status,
|
||||
account: state.accounts.get(state.me),
|
||||
account: selectOwnAccount(state),
|
||||
explicitAddressing,
|
||||
};
|
||||
|
||||
|
@ -250,7 +253,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 = {
|
||||
|
@ -621,7 +624,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;
|
||||
|
@ -631,8 +634,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;
|
||||
}
|
||||
|
||||
|
@ -778,12 +781,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);
|
||||
|
@ -798,12 +802,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);
|
||||
|
@ -827,7 +832,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
|
@ -0,0 +1,2 @@
|
|||
export { useSuggest } from './useSuggest';
|
||||
export { useVerify } from './useVerify';
|
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
|
@ -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
|
@ -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);
|
||||
};
|
||||
|
|
18
package.json
|
@ -28,9 +28,7 @@
|
|||
"lint": "${npm_execpath} run lint:js && ${npm_execpath} run lint:sass",
|
||||
"lint:js": "npx eslint --ext .js,.jsx,.cjs,.mjs,.ts,.tsx . --cache",
|
||||
"lint:sass": "npx stylelint app/styles/**/*.scss",
|
||||
"prepare": "husky install",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"build-storybook": "build-storybook"
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"browserslist": [
|
||||
|
@ -89,7 +87,7 @@
|
|||
"@types/react-datepicker": "^4.4.2",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/react-helmet": "^6.1.5",
|
||||
"@types/react-motion": "^0.0.33",
|
||||
"@types/react-motion": "^0.0.34",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-sparklines": "^1.7.2",
|
||||
"@types/react-swipeable-views": "^0.13.1",
|
||||
|
@ -209,15 +207,6 @@
|
|||
"@gitbeaker/node": "^35.8.0",
|
||||
"@jedmao/redux-mock-store": "^3.0.5",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
||||
"@storybook/addon-actions": "^6.5.16",
|
||||
"@storybook/addon-essentials": "^6.5.16",
|
||||
"@storybook/addon-interactions": "^6.5.16",
|
||||
"@storybook/addon-links": "^6.5.16",
|
||||
"@storybook/addon-postcss": "^2.0.0",
|
||||
"@storybook/builder-webpack5": "^6.5.16",
|
||||
"@storybook/manager-webpack5": "^6.5.16",
|
||||
"@storybook/react": "^6.5.16",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
|
@ -231,7 +220,7 @@
|
|||
"eslint": "^8.0.0",
|
||||
"eslint-plugin-compat": "^4.0.2",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jsdoc": "^43.1.1",
|
||||
"eslint-plugin-jsdoc": "^46.0.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-promise": "^6.0.0",
|
||||
"eslint-plugin-react": "^7.25.1",
|
||||
|
@ -246,7 +235,6 @@
|
|||
"raf": "^3.4.1",
|
||||
"react-intl-translations-manager": "^5.0.3",
|
||||
"react-refresh": "^0.14.0",
|
||||
"storybook-react-intl": "^1.1.1",
|
||||
"stylelint": "^14.0.0",
|
||||
"stylelint-config-standard-scss": "^6.1.0",
|
||||
"tailwindcss": "^3.3.1",
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from 'soapbox/components/ui';
|
||||
|
||||
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||
export default {
|
||||
title: 'UI/Button',
|
||||
component: Button,
|
||||
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
||||
argTypes: {
|
||||
text: { type: 'string', defaultValue: 'Button' },
|
||||
theme: { defaultValue: 'primary' },
|
||||
size: { defaultValue: 'md' },
|
||||
disabled: { defaultValue: false },
|
||||
block: { defaultValue: false },
|
||||
children: { table: { disable: true } },
|
||||
className: { table: { disable: true } },
|
||||
type: { table: { disable: true } },
|
||||
to: { table: { disable: true } },
|
||||
icon: { table: { disable: true } },
|
||||
onClick: { table: { disable: true } },
|
||||
},
|
||||
} as ComponentMeta<typeof Button>;
|
||||
|
||||
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
||||
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
Primary.args = {
|
||||
theme: 'primary',
|
||||
};
|
||||
|
||||
export const Secondary = Template.bind({});
|
||||
Secondary.args = {
|
||||
theme: 'secondary',
|
||||
};
|
||||
|
||||
export const Large = Template.bind({});
|
||||
Large.args = {
|
||||
size: 'lg',
|
||||
};
|
||||
|
||||
export const Small = Template.bind({});
|
||||
Small.args = {
|
||||
size: 'sm',
|
||||
};
|
|
@ -1,25 +0,0 @@
|
|||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Header } from './Header';
|
||||
|
||||
export default {
|
||||
title: 'Example/Header',
|
||||
component: Header,
|
||||
parameters: {
|
||||
// More on Story layout: https://storybook.js.org/docs/react/configure/story-layout
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} as ComponentMeta<typeof Header>;
|
||||
|
||||
const Template: ComponentStory<typeof Header> = (args) => <Header {...args} />;
|
||||
|
||||
export const LoggedIn = Template.bind({});
|
||||
LoggedIn.args = {
|
||||
user: {
|
||||
name: 'Jane Doe',
|
||||
},
|
||||
};
|
||||
|
||||
export const LoggedOut = Template.bind({});
|
||||
LoggedOut.args = {};
|
|
@ -1,57 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import Button from '../app/soapbox/components/ui/button/button';
|
||||
|
||||
import './header.css';
|
||||
|
||||
type User = {
|
||||
name: string
|
||||
};
|
||||
|
||||
interface HeaderProps {
|
||||
user?: User
|
||||
onLogin: () => void
|
||||
onLogout: () => void
|
||||
onCreateAccount: () => void
|
||||
}
|
||||
|
||||
export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => (
|
||||
<header>
|
||||
<div className='wrapper'>
|
||||
<div>
|
||||
<svg width='32' height='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'>
|
||||
<g fill='none' fillRule='evenodd'>
|
||||
<path
|
||||
d='M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z'
|
||||
fill='#FFF'
|
||||
/>
|
||||
<path
|
||||
d='M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z'
|
||||
fill='#555AB9'
|
||||
/>
|
||||
<path
|
||||
d='M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z'
|
||||
fill='#91BAF8'
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<h1>Acme</h1>
|
||||
</div>
|
||||
<div>
|
||||
{user ? (
|
||||
<>
|
||||
<span className='welcome'>
|
||||
Welcome, <b>{user.name}</b>!
|
||||
</span>
|
||||
<Button size='sm' onClick={onLogout} text='Log out' />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button size='sm' onClick={onLogin} text='Log in' />
|
||||
<Button theme='primary' size='sm' onClick={onCreateAccount} text='Sign up' />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
|
@ -1,211 +0,0 @@
|
|||
import { Meta } from '@storybook/addon-docs';
|
||||
import Code from './assets/code-brackets.svg';
|
||||
import Colors from './assets/colors.svg';
|
||||
import Comments from './assets/comments.svg';
|
||||
import Direction from './assets/direction.svg';
|
||||
import Flow from './assets/flow.svg';
|
||||
import Plugin from './assets/plugin.svg';
|
||||
import Repo from './assets/repo.svg';
|
||||
import StackAlt from './assets/stackalt.svg';
|
||||
|
||||
<Meta title="Example/Introduction" />
|
||||
|
||||
<style>
|
||||
{`
|
||||
.subheading {
|
||||
--mediumdark: '#999999';
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
letter-spacing: 6px;
|
||||
line-height: 24px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 12px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.link-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 620px) {
|
||||
.link-list {
|
||||
row-gap: 20px;
|
||||
column-gap: 20px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (-ms-high-contrast:none) {
|
||||
.link-list {
|
||||
display: -ms-grid;
|
||||
-ms-grid-columns: 1fr 1fr;
|
||||
-ms-grid-rows: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.link-item {
|
||||
display: block;
|
||||
padding: 20px 30px 20px 15px;
|
||||
border: 1px solid #00000010;
|
||||
border-radius: 5px;
|
||||
transition: background 150ms ease-out, border 150ms ease-out, transform 150ms ease-out;
|
||||
color: #333333;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.link-item:hover {
|
||||
border-color: #1EA7FD50;
|
||||
transform: translate3d(0, -3px, 0);
|
||||
box-shadow: rgba(0, 0, 0, 0.08) 0 3px 10px 0;
|
||||
}
|
||||
|
||||
.link-item:active {
|
||||
border-color: #1EA7FD;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.link-item strong {
|
||||
font-weight: 700;
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.link-item img {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
margin-right: 15px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.link-item span {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
display: inline-block;
|
||||
border-radius: 1em;
|
||||
font-size: 11px;
|
||||
line-height: 12px;
|
||||
font-weight: 700;
|
||||
background: #E7FDD8;
|
||||
color: #66BF3C;
|
||||
padding: 4px 12px;
|
||||
margin-right: 10px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.tip-wrapper {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.tip-wrapper code {
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
# Welcome to Storybook
|
||||
|
||||
Storybook helps you build UI components in isolation from your app's business logic, data, and context.
|
||||
That makes it easy to develop hard-to-reach states. Save these UI states as **stories** to revisit during development, testing, or QA.
|
||||
|
||||
Browse example stories now by navigating to them in the sidebar.
|
||||
View their code in the `stories` directory to learn how they work.
|
||||
We recommend building UIs with a [**component-driven**](https://componentdriven.org) process starting with atomic components and ending with pages.
|
||||
|
||||
<div className="subheading">Configure</div>
|
||||
|
||||
<div className="link-list">
|
||||
<a
|
||||
className="link-item"
|
||||
href="https://storybook.js.org/docs/react/addons/addon-types"
|
||||
target="_blank"
|
||||
>
|
||||
<img src={Plugin} alt="plugin" />
|
||||
<span>
|
||||
<strong>Presets for popular tools</strong>
|
||||
Easy setup for TypeScript, SCSS and more.
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="link-item"
|
||||
href="https://storybook.js.org/docs/react/configure/webpack"
|
||||
target="_blank"
|
||||
>
|
||||
<img src={StackAlt} alt="Build" />
|
||||
<span>
|
||||
<strong>Build configuration</strong>
|
||||
How to customize webpack and Babel
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="link-item"
|
||||
href="https://storybook.js.org/docs/react/configure/styling-and-css"
|
||||
target="_blank"
|
||||
>
|
||||
<img src={Colors} alt="colors" />
|
||||
<span>
|
||||
<strong>Styling</strong>
|
||||
How to load and configure CSS libraries
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="link-item"
|
||||
href="https://storybook.js.org/docs/react/get-started/setup#configure-storybook-for-your-stack"
|
||||
target="_blank"
|
||||
>
|
||||
<img src={Flow} alt="flow" />
|
||||
<span>
|
||||
<strong>Data</strong>
|
||||
Providers and mocking for data libraries
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="subheading">Learn</div>
|
||||
|
||||
<div className="link-list">
|
||||
<a className="link-item" href="https://storybook.js.org/docs" target="_blank">
|
||||
<img src={Repo} alt="repo" />
|
||||
<span>
|
||||
<strong>Storybook documentation</strong>
|
||||
Configure, customize, and extend
|
||||
</span>
|
||||
</a>
|
||||
<a className="link-item" href="https://storybook.js.org/tutorials/" target="_blank">
|
||||
<img src={Direction} alt="direction" />
|
||||
<span>
|
||||
<strong>In-depth guides</strong>
|
||||
Best practices from leading teams
|
||||
</span>
|
||||
</a>
|
||||
<a className="link-item" href="https://github.com/storybookjs/storybook" target="_blank">
|
||||
<img src={Code} alt="code" />
|
||||
<span>
|
||||
<strong>GitHub project</strong>
|
||||
View the source and add issues
|
||||
</span>
|
||||
</a>
|
||||
<a className="link-item" href="https://discord.gg/storybook" target="_blank">
|
||||
<img src={Comments} alt="comments" />
|
||||
<span>
|
||||
<strong>Discord chat</strong>
|
||||
Chat with maintainers and the community
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="tip-wrapper">
|
||||
<span className="tip">Tip</span>Edit the Markdown in{' '}
|
||||
<code>stories/Introduction.stories.mdx</code>
|
||||
</div>
|
|
@ -1,27 +0,0 @@
|
|||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { within, userEvent } from '@storybook/testing-library';
|
||||
import React from 'react';
|
||||
|
||||
import { Page } from './Page';
|
||||
|
||||
export default {
|
||||
title: 'Example/Page',
|
||||
component: Page,
|
||||
parameters: {
|
||||
// More on Story layout: https://storybook.js.org/docs/react/configure/story-layout
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} as ComponentMeta<typeof Page>;
|
||||
|
||||
const Template: ComponentStory<typeof Page> = (args) => <Page {...args} />;
|
||||
|
||||
export const LoggedOut = Template.bind({});
|
||||
|
||||
export const LoggedIn = Template.bind({});
|
||||
|
||||
// More on interaction testing: https://storybook.js.org/docs/react/writing-tests/interaction-testing
|
||||
LoggedIn.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const loginButton = await canvas.getByRole('button', { name: /Log in/i });
|
||||
await userEvent.click(loginButton);
|
||||
};
|
|
@ -1,73 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Header } from './Header';
|
||||
import './page.css';
|
||||
|
||||
type User = {
|
||||
name: string
|
||||
};
|
||||
|
||||
export const Page: React.FC = () => {
|
||||
const [user, setUser] = React.useState<User>();
|
||||
|
||||
return (
|
||||
<article>
|
||||
<Header
|
||||
user={user}
|
||||
onLogin={() => setUser({ name: 'Jane Doe' })}
|
||||
onLogout={() => setUser(undefined)}
|
||||
onCreateAccount={() => setUser({ name: 'Jane Doe' })}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<h2>Pages in Storybook</h2>
|
||||
<p>
|
||||
We recommend building UIs with a{' '}
|
||||
<a href='https://componentdriven.org' target='_blank' rel='noopener noreferrer'>
|
||||
<strong>component-driven</strong>
|
||||
</a>{' '}
|
||||
process starting with atomic components and ending with pages.
|
||||
</p>
|
||||
<p>
|
||||
Render pages with mock data. This makes it easy to build and review page states without
|
||||
needing to navigate to them in your app. Here are some handy patterns for managing page
|
||||
data in Storybook:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Use a higher-level connected component. Storybook helps you compose such data from the
|
||||
"args" of child component stories
|
||||
</li>
|
||||
<li>
|
||||
Assemble data in the page component from your services. You can mock these services out
|
||||
using Storybook.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Get a guided tutorial on component-driven development at{' '}
|
||||
<a href='https://storybook.js.org/tutorials/' target='_blank' rel='noopener noreferrer'>
|
||||
Storybook tutorials
|
||||
</a>
|
||||
. Read more in the{' '}
|
||||
<a href='https://storybook.js.org/docs' target='_blank' rel='noopener noreferrer'>
|
||||
docs
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<div className='tip-wrapper'>
|
||||
<span className='tip'>Tip</span> Adjust the width of the canvas with the{' '}
|
||||
<svg width='10' height='10' viewBox='0 0 12 12' xmlns='http://www.w3.org/2000/svg'>
|
||||
<g fill='none' fillRule='evenodd'>
|
||||
<path
|
||||
d='M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0 01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0 010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z'
|
||||
id='a'
|
||||
fill='#999'
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
Viewports addon in the toolbar
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
);
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/code-brackets</title><g id="illustration/code-brackets" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#87E6E5" d="M11.4139325,12 C11.7605938,12 12,12.5059743 12,13.3779712 L12,17.4951758 L6.43502246,23.3839989 C5.85499251,23.9978337 5.85499251,25.0021663 6.43502246,25.6160011 L12,31.5048242 L12,35.6220288 C12,36.4939606 11.7605228,37 11.4139325,37 C11.2725831,37 11.1134406,36.9158987 10.9453839,36.7379973 L0.435022463,25.6160011 C-0.145007488,25.0021663 -0.145007488,23.9978337 0.435022463,23.3839989 L10.9453839,12.2620027 C11.1134051,12.0841663 11.2725831,12 11.4139325,12 Z M36.5860675,12 C36.7274169,12 36.8865594,12.0841013 37.0546161,12.2620027 L47.5649775,23.3839989 C48.1450075,23.9978337 48.1450075,25.0021663 47.5649775,25.6160011 L37.0546161,36.7379973 C36.8865949,36.9158337 36.7274169,37 36.5860675,37 C36.2394062,37 36,36.4940257 36,35.6220288 L36,31.5048242 L41.5649775,25.6160011 C42.1450075,25.0021663 42.1450075,23.9978337 41.5649775,23.3839989 L36,17.4951758 L36,13.3779712 C36,12.5060394 36.2394772,12 36.5860675,12 Z"/><rect id="Rectangle-7-Copy-5" width="35.57" height="4" x="5.009" y="22.662" fill="#A0DB77" rx="2" transform="translate(22.793959, 24.662305) rotate(-75.000000) translate(-22.793959, -24.662305)"/></g></svg>
|
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 8.3 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/comments</title><g id="illustration/comments" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Path" fill="#96D07C" d="M2.52730803,17.9196415 C2.44329744,17.9745167 2.36370847,18.000488 2.29303375,18.000488 C2.1197031,18.000488 2,17.8443588 2,17.5752855 L2,4 C2,1.790861 3.790861,3.23296945e-13 6,3.23296945e-13 L33.9995117,3.23296945e-13 C36.2086507,3.23296945e-13 37.9995117,1.790861 37.9995117,4 L37.9995117,9.999512 C37.9995117,12.208651 36.2086507,13.999512 33.9995117,13.999512 L8,13.999512 C7.83499225,13.999512 7.6723181,13.9895206 7.51254954,13.9701099 L2.52730803,17.9196415 Z"/><path id="Path" fill="#73E1E0" d="M7.51066,44.9703679 L2.52730803,47.9186655 C2.44329744,47.9735407 2.36370847,47.999512 2.29303375,47.999512 C2.1197031,47.999512 2,47.8433828 2,47.5743095 L2,35 C2,32.790861 3.790861,31 6,31 L26,31 C28.209139,31 30,32.790861 30,35 L30,41 C30,43.209139 28.209139,45 26,45 L8,45 C7.8343417,45 7.67103544,44.9899297 7.51066,44.9703679 Z"/><path id="Path" fill="#FFD476" d="M46,19.5 L46,33.0747975 C46,33.3438708 45.8802969,33.5 45.7069663,33.5 C45.6362915,33.5 45.5567026,33.4740287 45.472692,33.4191535 L40.4887103,29.4704446 C40.3285371,29.489956 40.1654415,29.5 40,29.5 L18,29.5 C15.790861,29.5 14,27.709139 14,25.5 L14,19.5 C14,17.290861 15.790861,15.5 18,15.5 L42,15.5 C44.209139,15.5 46,17.290861 46,19.5 Z"/></g></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/direction</title><g id="illustration/direction" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#FFD476" d="M23.4917015,33.6030641 L2.93840258,31.4321033 C2.38917316,31.3740904 1.99096346,30.8818233 2.04897631,30.3325939 C2.0747515,30.0885705 2.18934861,29.8625419 2.37095722,29.6975265 L34.2609105,0.721285325 C34.6696614,0.349881049 35.3021022,0.38015648 35.6735064,0.788907393 C35.9232621,1.06377731 36.0001133,1.45442096 35.8730901,1.80341447 L24.5364357,32.9506164 C24.3793473,33.3822133 23.9484565,33.6513092 23.4917015,33.6030641 L23.4917015,33.6030641 Z"/><path id="Combined-Shape-Copy" fill="#FFC445" d="M24.3163597,33.2881029 C24.0306575,33.0138462 23.9337246,32.5968232 24.069176,32.2246735 L35.091923,1.9399251 C35.2266075,1.56988243 35.5659249,1.31333613 35.9586669,1.28460955 C36.5094802,1.24432106 36.9886628,1.65818318 37.0289513,2.20899647 L40.2437557,46.1609256 C40.2644355,46.4436546 40.1641446,46.7218752 39.9678293,46.9263833 C39.5853672,47.3248067 38.9523344,47.3377458 38.5539111,46.9552837 L24.3163597,33.2881029 L24.3163597,33.2881029 Z"/></g></svg>
|
Before Width: | Height: | Size: 1.3 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/flow</title><g id="illustration/flow" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#79C9FC" fill-rule="nonzero" d="M30,29 C32.7614237,29 35,26.7614237 35,24 C35,14.6111593 27.3888407,7 18,7 C8.61115925,7 1,14.6111593 1,24 C1,33.3888407 8.61115925,41 18,41 C19.3333404,41 20.6447683,40.8466238 21.9154603,40.5471706 C19.5096374,39.3319645 17.5510566,37.8612875 16.0456579,36.1314815 C14.1063138,33.9030427 12.769443,31.0725999 12.0293806,27.6556449 C11.360469,26.565281 11,25.3082308 11,24 C11,20.1340068 14.1340068,17 18,17 C21.8659932,17 25,20.1340068 25,24 C25,26.125 27.7040312,29 30,29 Z"/><path id="Combined-Shape-Copy" fill="#FFC445" fill-rule="nonzero" d="M42,29 C44.7614237,29 47,26.7614237 47,24 C47,14.6111593 39.3888407,7 30,7 C20.6111593,7 13,14.6111593 13,24 C13,33.3888407 20.6111593,41 30,41 C31.3333404,41 32.6447683,40.8466238 33.9154603,40.5471706 C31.5096374,39.3319645 29.4051056,37.9781963 28.0456579,36.1314815 C26.0625,33.4375 23,27.1875 23,24 C23,20.1340068 26.1340068,17 30,17 C33.8659932,17 37,20.1340068 37,24 C37.02301,26.3435241 39.7040312,29 42,29 Z" transform="translate(30.000000, 24.000000) scale(-1, -1) translate(-30.000000, -24.000000)"/></g></svg>
|
Before Width: | Height: | Size: 1.4 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/plugin</title><g id="illustration/plugin" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#79C9FC" d="M26,15.3994248 C26,15.4091303 26,15.4188459 26,15.4285714 L26,21.4694881 C25.8463595,21.4969567 25.6941676,21.51275 25.5873784,21.51275 C25.4974117,21.51275 25.4230979,21.4768034 25.377756,21.4206259 L25.2660784,21.2822603 L25.1317423,21.1657666 C24.2436317,20.3956144 23.100098,19.9633214 21.895551,19.9633214 C19.2039137,19.9633214 17,22.1075558 17,24.7804643 C17,27.4533728 19.2039137,29.5976071 21.895551,29.5976071 C23.1972122,29.5976071 24.3149423,29.2878193 25.1231445,28.3613697 C25.4542273,27.9818463 25.568273,27.9073214 25.5873784,27.9073214 C25.681532,27.9073214 25.8352452,27.9239643 26,27.9524591 L26,32.5714286 C26,32.5811541 26,32.5908697 26,32.6005752 L26,33 C26,35.209139 24.209139,37 22,37 L4,37 C1.790861,37 0,35.209139 0,33 L0,15 C0,12.790861 1.790861,11 4,11 L22,11 C24.209139,11 26,12.790861 26,15 L26,15.3994248 Z"/><path id="Path" fill="#87E6E5" d="M27.9998779,32.5714286 C27.9998779,33.3604068 28.6572726,34 29.4682101,34 L46.5315458,34 C47.3424832,34 47.9998779,33.3604068 47.9998779,32.5714286 L47.9998779,15.4285714 C47.9998779,14.6395932 47.3424832,14 46.5315458,14 L29.4682101,14 C28.6572726,14 27.9998779,14.6395932 27.9998779,15.4285714 L27.9998779,21.8355216 C27.9334367,22.2650514 27.8567585,22.6454496 27.746391,22.8084643 C27.4245309,23.2838571 26.2402709,23.51275 25.5873784,23.51275 C24.8705773,23.51275 24.2322714,23.1857725 23.8214379,22.6767605 C23.3096996,22.2329909 22.6349941,21.9633214 21.895551,21.9633214 C20.2963823,21.9633214 19,23.2245992 19,24.7804643 C19,26.3363293 20.2963823,27.5976071 21.895551,27.5976071 C22.5398535,27.5976071 23.2399343,27.477727 23.6160247,27.0466112 C24.1396029,26.4464286 24.7367044,25.9073214 25.5873784,25.9073214 C26.2402709,25.9073214 27.5912951,26.1766031 27.8226692,26.6116071 C27.8819199,26.7230038 27.9403239,26.921677 27.9998779,27.1556219 L27.9998779,32.5714286 Z"/></g></svg>
|
Before Width: | Height: | Size: 2.1 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/repo</title><g id="illustration/repo" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Rectangle-62-Copy" fill="#B7F0EF" d="M27.2217723,9.04506931 L41.2217723,6.2682098 C43.3886973,5.83840648 45.4937616,7.2466219 45.9235649,9.41354696 C45.9743993,9.66983721 46,9.93049166 46,10.1917747 L46,32.581381 C46,34.4904961 44.650862,36.1335143 42.7782277,36.5049459 L28.7782277,39.2818054 C26.6113027,39.7116087 24.5062384,38.3033933 24.0764351,36.1364682 C24.0256007,35.880178 24,35.6195235 24,35.3582405 L24,12.9686342 C24,11.0595191 25.349138,9.4165009 27.2217723,9.04506931 Z" opacity=".7"/><path id="Combined-Shape" fill="#87E6E5" d="M6.77822775,6.2682098 L20.7782277,9.04506931 C22.650862,9.4165009 24,11.0595191 24,12.9686342 L24,35.3582405 C24,37.5673795 22.209139,39.3582405 20,39.3582405 C19.738717,39.3582405 19.4780625,39.3326398 19.2217723,39.2818054 L5.22177225,36.5049459 C3.34913798,36.1335143 2,34.4904961 2,32.581381 L2,10.1917747 C2,7.98263571 3.790861,6.19177471 6,6.19177471 C6.26128305,6.19177471 6.5219375,6.21737537 6.77822775,6.2682098 Z"/><path id="Rectangle-63-Copy-2" fill="#61C1FD" d="M22,10 C23.1666667,10.2291667 24.0179036,10.625 24.5537109,11.1875 C25.0895182,11.75 25.5716146,12.875 26,14.5625 C26,29.3020833 26,37.5208333 26,39.21875 C26,40.9166667 26.4241536,42.9583333 27.2724609,45.34375 L24.5537109,41.875 L22.9824219,45.34375 C22.327474,43.1979167 22,41.2291667 22,39.4375 C22,37.6458333 22,27.8333333 22,10 Z"/></g></svg>
|
Before Width: | Height: | Size: 1.6 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/stackalt</title><g id="illustration/stackalt" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#FFAE00" d="M23.8628277,0 L23.8628277,48 L3.32291648,36.2491883 L3.32155653,11.9499781 L23.8628277,0 Z M23.8670509,0 L44.408322,11.9499781 L44.4069621,36.2491883 L23.8670509,48 L23.8670509,0 Z" opacity=".196"/><path id="Rectangle-46-Copy-3" fill="#66BF3C" d="M15.8232279,19.1155258 L24.7368455,21.4714881 C29.6053842,22.7582937 33.4077423,26.5606518 34.694548,31.4291905 L37.0505103,40.3428082 C37.6150232,42.4786032 36.3412474,44.6676353 34.2054524,45.2321482 C33.5569474,45.4035549 32.87575,45.4091235 32.2245294,45.2483418 L23.3459013,43.0562718 C18.2976962,41.809906 14.3561301,37.8683399 13.1097642,32.8201348 L10.9176943,23.9415066 C10.3881737,21.7967682 11.6975664,19.6288529 13.8423049,19.0993322 C14.4935255,18.9385505 15.1747229,18.9441191 15.8232279,19.1155258 Z" opacity=".5" transform="translate(23.999997, 32.166058) rotate(-45.000000) translate(-23.999997, -32.166058)"/><path id="Rectangle-46-Copy-2" fill="#FFAE00" d="M15.8232279,11.2216893 L24.7368455,13.5776516 C29.6053842,14.8644572 33.4077423,18.6668153 34.694548,23.5353541 L37.0505103,32.4489717 C37.6150232,34.5847667 36.3412474,36.7737988 34.2054524,37.3383117 C33.5569474,37.5097184 32.87575,37.515287 32.2245294,37.3545053 L23.3459013,35.1624353 C18.2976962,33.9160695 14.3561301,29.9745034 13.1097642,24.9262983 L10.9176943,16.0476701 C10.3881737,13.9029317 11.6975664,11.7350164 13.8423049,11.2054957 C14.4935255,11.044714 15.1747229,11.0502826 15.8232279,11.2216893 Z" opacity=".5" transform="translate(23.999997, 24.272222) rotate(-45.000000) translate(-23.999997, -24.272222)"/><path id="Rectangle-46-Copy" fill="#FC521F" d="M15.8232279,3.32785281 L24.7368455,5.68381509 C29.6053842,6.97062075 33.4077423,10.7729788 34.694548,15.6415176 L37.0505103,24.5551352 C37.6150232,26.6909302 36.3412474,28.8799623 34.2054524,29.4444752 C33.5569474,29.6158819 32.87575,29.6214505 32.2245294,29.4606688 L23.3459013,27.2685988 C18.2976962,26.022233 14.3561301,22.0806669 13.1097642,17.0324618 L10.9176943,8.15383364 C10.3881737,6.00909519 11.6975664,3.84117987 13.8423049,3.31165925 C14.4935255,3.15087753 15.1747229,3.15644615 15.8232279,3.32785281 Z" opacity=".5" transform="translate(23.999997, 16.378385) rotate(-45.000000) translate(-23.999997, -16.378385)"/></g></svg>
|
Before Width: | Height: | Size: 2.5 KiB |
|
@ -1,32 +0,0 @@
|
|||
.wrapper {
|
||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding: 15px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
svg {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 900;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
margin: 6px 0 6px 10px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
button + button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.welcome {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
section {
|
||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
padding: 48px 20px;
|
||||
margin: 0 auto;
|
||||
max-width: 600px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
section h2 {
|
||||
font-weight: 900;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
margin: 0 0 4px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
section p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
section a {
|
||||
text-decoration: none;
|
||||
color: #1ea7fd;
|
||||
}
|
||||
|
||||
section ul {
|
||||
padding-left: 30px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
section li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
section .tip {
|
||||
display: inline-block;
|
||||
border-radius: 1em;
|
||||
font-size: 11px;
|
||||
line-height: 12px;
|
||||
font-weight: 700;
|
||||
background: #e7fdd8;
|
||||
color: #66bf3c;
|
||||
padding: 4px 12px;
|
||||
margin-right: 10px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
section .tip-wrapper {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
section .tip-wrapper svg {
|
||||
display: inline-block;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
margin-right: 4px;
|
||||
vertical-align: top;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
section .tip-wrapper svg path {
|
||||
fill: #1ea7fd;
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
/* Soapbox default colors for Tailwind.
|
||||
TODO: generate this file by reading a mock soapbox.json. */
|
||||
:root {
|
||||
--color-primary-50: 242 249 253;
|
||||
--color-primary-100: 230 243 251;
|
||||
--color-primary-200: 192 224 245;
|
||||
--color-primary-300: 79 168 228;
|
||||
--color-primary-400: 54 155 224;
|
||||
--color-primary-500: 4 130 216;
|
||||
--color-primary-600: 4 117 194;
|
||||
--color-primary-700: 3 98 162;
|
||||
--color-primary-800: 1 39 65;
|
||||
--color-primary-900: 1 25 41;
|
||||
--color-success-50: 240 253 244;
|
||||
--color-success-100: 220 252 231;
|
||||
--color-success-200: 187 247 208;
|
||||
--color-success-300: 134 239 172;
|
||||
--color-success-400: 74 222 128;
|
||||
--color-success-500: 34 197 94;
|
||||
--color-success-600: 22 163 74;
|
||||
--color-success-700: 21 128 61;
|
||||
--color-success-800: 22 101 52;
|
||||
--color-success-900: 20 83 45;
|
||||
--color-danger-50: 254 242 242;
|
||||
--color-danger-100: 254 226 226;
|
||||
--color-danger-200: 254 202 202;
|
||||
--color-danger-300: 252 165 165;
|
||||
--color-danger-400: 248 113 113;
|
||||
--color-danger-500: 239 68 68;
|
||||
--color-danger-600: 220 38 38;
|
||||
--color-danger-700: 185 28 28;
|
||||
--color-danger-800: 153 27 27;
|
||||
--color-danger-900: 127 29 29;
|
||||
--color-gray-50: 248 250 250;
|
||||
--color-gray-100: 241 244 246;
|
||||
--color-gray-200: 221 228 232;
|
||||
--color-gray-300: 158 178 191;
|
||||
--color-gray-400: 145 167 181;
|
||||
--color-gray-500: 117 145 163;
|
||||
--color-gray-600: 105 131 147;
|
||||
--color-gray-700: 88 109 122;
|
||||
--color-gray-800: 35 44 49;
|
||||
--color-gray-900: 22 28 31;
|
||||
--color-gradient-start: 4 130 216;
|
||||
--color-accent-50: 243 251 253;
|
||||
--color-accent-100: 231 247 250;
|
||||
--color-accent-200: 195 236 244;
|
||||
--color-accent-300: 88 202 223;
|
||||
--color-accent-400: 64 194 218;
|
||||
--color-accent-500: 16 179 209;
|
||||
--color-accent-600: 14 161 188;
|
||||
--color-accent-700: 12 134 157;
|
||||
--color-accent-800: 5 54 63;
|
||||
--color-accent-900: 3 34 40;
|
||||
--color-greentext: 120 153 34;
|
||||
--color-secondary-50: 243 251 253;
|
||||
--color-secondary-100: 231 247 250;
|
||||
--color-secondary-200: 195 236 244;
|
||||
--color-secondary-300: 88 202 223;
|
||||
--color-secondary-400: 64 194 218;
|
||||
--color-secondary-500: 16 179 209;
|
||||
--color-secondary-600: 14 161 188;
|
||||
--color-secondary-700: 12 134 157;
|
||||
--color-secondary-800: 5 54 63;
|
||||
--color-secondary-900: 3 34 40;
|
||||
--color-accent-blue: 4 117 194;
|
||||
--color-gradient-end: 16 179 209;
|
||||
}
|