Merge remote-tracking branch 'origin/develop' into reports-tailwind

environments/review-reports-ta-uwejzk/deployments/1600
Alex Gleason 2 years ago
commit 0e16bc13c5
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7

@ -4,7 +4,7 @@ import { defineMessages, useIntl } from 'react-intl';
import IconButton from 'soapbox/components/icon-button';
import BundleContainer from 'soapbox/features/ui/containers/bundle-container';
import { DatePicker } from 'soapbox/features/ui/util/async-components';
import { useAppSelector, useFeatures } from 'soapbox/hooks';
import { useInstance, useFeatures } from 'soapbox/hooks';
const messages = defineMessages({
birthdayPlaceholder: { id: 'edit_profile.fields.birthday_placeholder', defaultMessage: 'Your birthday' },
@ -23,9 +23,10 @@ interface IBirthdayInput {
const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required }) => {
const intl = useIntl();
const features = useFeatures();
const instance = useInstance();
const supportsBirthdays = features.birthdays;
const minAge = useAppSelector((state) => state.instance.pleroma.getIn(['metadata', 'birthday_min_age'])) as number;
const minAge = instance.pleroma.getIn(['metadata', 'birthday_min_age']) as number;
const maxDate = useMemo(() => {
if (!supportsBirthdays) return null;

@ -3,7 +3,7 @@ import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { Banner, Button, HStack, Stack, Text } from 'soapbox/components/ui';
import { useAppSelector, useSoapboxConfig } from 'soapbox/hooks';
import { useAppSelector, useInstance, useSoapboxConfig } from 'soapbox/hooks';
const acceptedGdpr = !!localStorage.getItem('soapbox:gdpr');
@ -13,9 +13,9 @@ const GdprBanner: React.FC = () => {
const [shown, setShown] = useState<boolean>(acceptedGdpr);
const [slideout, setSlideout] = useState(false);
const instance = useInstance();
const soapbox = useSoapboxConfig();
const isLoggedIn = useAppSelector(state => !!state.me);
const siteTitle = useAppSelector(state => state.instance.title);
const handleAccept = () => {
localStorage.setItem('soapbox:gdpr', 'true');
@ -34,14 +34,14 @@ const GdprBanner: React.FC = () => {
<div className='flex flex-col space-y-4 lg:space-y-0 lg:space-x-4 rtl:space-x-reverse lg:flex-row lg:items-center lg:justify-between'>
<Stack space={2}>
<Text size='xl' weight='bold'>
<FormattedMessage id='gdpr.title' defaultMessage='{siteTitle} uses cookies' values={{ siteTitle }} />
<FormattedMessage id='gdpr.title' defaultMessage='{siteTitle} uses cookies' values={{ siteTitle: instance.title }} />
</Text>
<Text weight='medium' className='opacity-60'>
<FormattedMessage
id='gdpr.message'
defaultMessage="{siteTitle} uses session cookies, which are essential to the website's functioning."
values={{ siteTitle }}
values={{ siteTitle: instance.title }}
/>
</Text>
</Stack>

@ -1,7 +1,7 @@
import React from 'react';
import { Helmet as ReactHelmet } from 'react-helmet';
import { useAppSelector, useSettings } from 'soapbox/hooks';
import { useAppSelector, useInstance, useSettings } from 'soapbox/hooks';
import { RootState } from 'soapbox/store';
import FaviconService from 'soapbox/utils/favicon-service';
@ -16,7 +16,7 @@ const getNotifTotals = (state: RootState): number => {
};
const Helmet: React.FC = ({ children }) => {
const title = useAppSelector((state) => state.instance.title);
const instance = useInstance();
const unreadCount = useAppSelector((state) => getNotifTotals(state));
const demetricator = useSettings().get('demetricator');
@ -40,8 +40,8 @@ const Helmet: React.FC = ({ children }) => {
return (
<ReactHelmet
titleTemplate={addCounter(`%s | ${title}`)}
defaultTitle={addCounter(title)}
titleTemplate={addCounter(`%s | ${instance.title}`)}
defaultTitle={addCounter(instance.title)}
defer={false}
>
{children}

@ -63,9 +63,9 @@
@apply w-5 h-5;
}
/* Markdown images */
[data-markup] img:not(.emojione):not([width][height]) {
@apply w-full h-72 object-contain rounded-lg overflow-hidden my-4 block;
/* Hide Markdown images (Pleroma) */
[data-markup] img:not(.emojione) {
@apply hidden;
}
/* User setting to underline links */

@ -1,11 +1,9 @@
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { getSettings } from 'soapbox/actions/settings';
import DropdownMenu from 'soapbox/containers/dropdown-menu-container';
import ComposeButton from 'soapbox/features/ui/components/compose-button';
import { useAppSelector, useOwnAccount } from 'soapbox/hooks';
import { getFeatures } from 'soapbox/utils/features';
import { useAppSelector, useFeatures, useOwnAccount, useSettings } from 'soapbox/hooks';
import SidebarNavigationLink from './sidebar-navigation-link';
@ -22,16 +20,14 @@ const messages = defineMessages({
const SidebarNavigation = () => {
const intl = useIntl();
const instance = useAppSelector((state) => state.instance);
const settings = useAppSelector((state) => getSettings(state));
const features = useFeatures();
const settings = useSettings();
const account = useOwnAccount();
const notificationCount = useAppSelector((state) => state.notifications.unread);
const chatsCount = useAppSelector((state) => state.chats.items.reduce((acc, curr) => acc + Math.min(curr.unread || 0, 1), 0));
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count());
const features = getFeatures(instance);
const makeMenu = (): Menu => {
const menu: Menu = [];

@ -189,7 +189,9 @@ const StatusList: React.FC<IStatusList> = ({
if (statusId === null) {
acc.push(renderLoadGap(index));
} else if (statusId.startsWith('末suggestions-')) {
acc.push(renderFeedSuggestions());
if (soapboxConfig.feedInjection) {
acc.push(renderFeedSuggestions());
}
} else if (statusId.startsWith('末pending-')) {
acc.push(renderPendingStatus(statusId));
} else {

@ -20,6 +20,11 @@ describe('<SensitiveContentOverlay />', () => {
expect(screen.getByTestId('sensitive-overlay')).toHaveTextContent('Sensitive content');
});
it('does not allow user to delete the status', () => {
render(<SensitiveContentOverlay status={status} />);
expect(screen.queryAllByTestId('icon-button')).toHaveLength(0);
});
it('can be toggled', () => {
render(<SensitiveContentOverlay status={status} />);
@ -43,6 +48,11 @@ describe('<SensitiveContentOverlay />', () => {
expect(screen.getByTestId('sensitive-overlay')).toHaveTextContent('Content Under Review');
});
it('allows the user to delete the status', () => {
render(<SensitiveContentOverlay status={status} />);
expect(screen.getByTestId('icon-button')).toBeInTheDocument();
});
it('can be toggled', () => {
render(<SensitiveContentOverlay status={status} />);

@ -1,8 +1,11 @@
import classNames from 'clsx';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useSettings, useSoapboxConfig } from 'soapbox/hooks';
import { openModal } from 'soapbox/actions/modals';
import { deleteStatus } from 'soapbox/actions/statuses';
import DropdownMenu from 'soapbox/containers/dropdown-menu-container';
import { useAppDispatch, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks';
import { defaultMediaVisibility } from 'soapbox/utils/status';
import { Button, HStack, Text } from '../ui';
@ -10,6 +13,10 @@ import { Button, HStack, Text } from '../ui';
import type { Status as StatusEntity } from 'soapbox/types/entities';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteHeading: { id: 'confirmations.delete.heading', defaultMessage: 'Delete post' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this post?' },
hide: { id: 'moderation_overlay.hide', defaultMessage: 'Hide content' },
sensitiveTitle: { id: 'status.sensitive_warning', defaultMessage: 'Sensitive content' },
underReviewTitle: { id: 'moderation_overlay.title', defaultMessage: 'Content Under Review' },
@ -27,15 +34,17 @@ interface ISensitiveContentOverlay {
const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveContentOverlay>((props, ref) => {
const { onToggleVisibility, status } = props;
const isUnderReview = status.visibility === 'self';
const settings = useSettings();
const displayMedia = settings.get('displayMedia') as string;
const account = useOwnAccount();
const dispatch = useAppDispatch();
const intl = useIntl();
const settings = useSettings();
const { links } = useSoapboxConfig();
const isUnderReview = status.visibility === 'self';
const isOwnStatus = status.getIn(['account', 'id']) === account?.id;
const displayMedia = settings.get('displayMedia') as string;
const [visible, setVisible] = useState<boolean>(defaultMediaVisibility(status, displayMedia));
const toggleVisibility = (event: React.MouseEvent<HTMLButtonElement>) => {
@ -48,6 +57,32 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
}
};
const handleDeleteStatus = () => {
const deleteModal = settings.get('deleteModal');
if (!deleteModal) {
dispatch(deleteStatus(status.id, false));
} else {
dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/trash.svg'),
heading: intl.formatMessage(messages.deleteHeading),
message: intl.formatMessage(messages.deleteMessage),
confirm: intl.formatMessage(messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.id, false)),
}));
}
};
const menu = useMemo(() => {
return [
{
text: intl.formatMessage(messages.delete),
action: handleDeleteStatus,
icon: require('@tabler/icons/trash.svg'),
destructive: true,
},
];
}, []);
useEffect(() => {
if (typeof props.visible !== 'undefined') {
setVisible(!!props.visible);
@ -122,6 +157,13 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
>
{intl.formatMessage(messages.show)}
</Button>
{(isUnderReview && isOwnStatus) ? (
<DropdownMenu
items={menu}
src={require('@tabler/icons/dots.svg')}
/>
) : null}
</HStack>
</div>
)}

@ -2,15 +2,14 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import ThumbNavigationLink from 'soapbox/components/thumb-navigation-link';
import { useAppSelector, useOwnAccount } from 'soapbox/hooks';
import { getFeatures } from 'soapbox/utils/features';
import { useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks';
const ThumbNavigation: React.FC = (): JSX.Element => {
const account = useOwnAccount();
const notificationCount = useAppSelector((state) => state.notifications.unread);
const chatsCount = useAppSelector((state) => state.chats.items.reduce((acc, curr) => acc + Math.min(curr.unread || 0, 1), 0));
const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count());
const features = getFeatures(useAppSelector((state) => state.instance));
const features = useFeatures();
/** Conditionally render the supported messages link */
const renderMessagesLink = (): React.ReactNode => {

@ -51,7 +51,7 @@ const families = {
};
export type Sizes = keyof typeof sizes
type Tags = 'abbr' | 'p' | 'span' | 'pre' | 'time' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'label' | 'blockquote'
type Tags = 'abbr' | 'p' | 'span' | 'pre' | 'time' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'label' | 'div' | 'blockquote'
type Directions = 'ltr' | 'rtl'
interface IText extends Pick<React.HTMLAttributes<HTMLParagraphElement>, 'dangerouslySetInnerHTML' | 'tabIndex' | 'lang'> {

@ -37,6 +37,7 @@ import {
useSettings,
useTheme,
useLocale,
useInstance,
} from 'soapbox/hooks';
import MESSAGES from 'soapbox/locales/messages';
import { queryClient } from 'soapbox/queries/client';
@ -85,7 +86,7 @@ const loadInitial = () => {
const SoapboxMount = () => {
useCachedLocationHandler();
const me = useAppSelector(state => state.me);
const instance = useAppSelector(state => state.instance);
const instance = useInstance();
const account = useOwnAccount();
const soapboxConfig = useSoapboxConfig();
const features = useFeatures();

@ -11,9 +11,8 @@ import { expandAccountMediaTimeline } from 'soapbox/actions/timelines';
import LoadMore from 'soapbox/components/load-more';
import MissingIndicator from 'soapbox/components/missing-indicator';
import { Column, Spinner } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
import { getAccountGallery, findAccountByUsername } from 'soapbox/selectors';
import { getFeatures } from 'soapbox/utils/features';
import MediaItem from './components/media-item';
@ -38,11 +37,11 @@ const LoadMoreMedia: React.FC<ILoadMoreMedia> = ({ maxId, onLoadMore }) => {
const AccountGallery = () => {
const dispatch = useAppDispatch();
const { username } = useParams<{ username: string }>();
const features = useFeatures();
const { accountId, unavailable, accountUsername } = useAppSelector((state) => {
const me = state.me;
const accountFetchError = (state.accounts.get(-1)?.username || '').toLowerCase() === username.toLowerCase();
const features = getFeatures(state.instance);
let accountId: string | -1 | null = -1;
let accountUsername = username;

@ -9,7 +9,7 @@ import {
RadioGroup,
RadioItem,
} from 'soapbox/features/forms';
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
import { useAppDispatch, useInstance } from 'soapbox/hooks';
import type { Instance } from 'soapbox/types/entities';
@ -42,8 +42,9 @@ const modeFromInstance = (instance: Instance): RegistrationMode => {
const RegistrationModePicker: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const instance = useInstance();
const mode = useAppSelector(state => modeFromInstance(state.instance));
const mode = modeFromInstance(instance);
const onChange: React.ChangeEventHandler<HTMLInputElement> = e => {
const config = generateConfig(e.target.value as RegistrationMode);

@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
import { getSubscribersCsv, getUnsubscribersCsv, getCombinedCsv } from 'soapbox/actions/email-list';
import { Text } from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch, useOwnAccount, useFeatures } from 'soapbox/hooks';
import { useAppDispatch, useOwnAccount, useFeatures, useInstance } from 'soapbox/hooks';
import sourceCode from 'soapbox/utils/code';
import { parseVersion } from 'soapbox/utils/features';
import { isNumber } from 'soapbox/utils/numbers';
@ -27,7 +27,7 @@ const download = (response: AxiosResponse, filename: string) => {
const Dashboard: React.FC = () => {
const dispatch = useAppDispatch();
const instance = useAppSelector(state => state.instance);
const instance = useInstance();
const features = useFeatures();
const account = useOwnAccount();

@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
import { Avatar, Card, HStack, Icon, IconButton, Stack, Text } from 'soapbox/components/ui';
import StatusCard from 'soapbox/features/status/components/card';
import { useAppSelector } from 'soapbox/hooks';
import { useInstance } from 'soapbox/hooks';
import type { Ad as AdEntity } from 'soapbox/types/soapbox';
@ -15,7 +15,7 @@ interface IAd {
/** Displays an ad in sponsored post format. */
const Ad: React.FC<IAd> = ({ ad }) => {
const queryClient = useQueryClient();
const instance = useAppSelector(state => state.instance);
const instance = useInstance();
const timer = useRef<NodeJS.Timeout | undefined>(undefined);
const infobox = useRef<HTMLDivElement>(null);

@ -5,9 +5,8 @@ import { addToAliases } from 'soapbox/actions/aliases';
import AccountComponent from 'soapbox/components/account';
import IconButton from 'soapbox/components/icon-button';
import { HStack } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
import { getFeatures } from 'soapbox/utils/features';
import type { List as ImmutableList } from 'immutable';
@ -23,21 +22,19 @@ interface IAccount {
const Account: React.FC<IAccount> = ({ accountId, aliases }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const features = useFeatures();
const getAccount = useCallback(makeGetAccount(), []);
const account = useAppSelector((state) => getAccount(state, accountId));
const added = useAppSelector((state) => {
const instance = state.instance;
const features = getFeatures(instance);
const me = useAppSelector((state) => state.me);
const added = useAppSelector((state) => {
const account = getAccount(state, accountId);
const apId = account?.pleroma.get('ap_id');
const name = features.accountMoving ? account?.acct : apId;
return aliases.includes(name);
});
const me = useAppSelector((state) => state.me);
const handleOnAdd = () => dispatch(addToAliases(account!));

@ -6,9 +6,7 @@ import { fetchAliases, removeFromAliases } from 'soapbox/actions/aliases';
import Icon from 'soapbox/components/icon';
import ScrollableList from 'soapbox/components/scrollable-list';
import { CardHeader, CardTitle, Column, HStack, Text } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
import { getFeatures } from 'soapbox/utils/features';
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks';
import Account from './components/account';
import Search from './components/search';
@ -22,22 +20,21 @@ const messages = defineMessages({
delete: { id: 'column.aliases.delete', defaultMessage: 'Delete' },
});
const getAccount = makeGetAccount();
const Aliases = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const features = useFeatures();
const account = useOwnAccount();
const aliases = useAppSelector((state) => {
const me = state.me as string;
const account = getAccount(state, me);
const instance = state.instance;
const features = getFeatures(instance);
if (features.accountMoving) return state.aliases.aliases.items;
return account!.pleroma.get('also_known_as');
if (features.accountMoving) {
return state.aliases.aliases.items;
} else {
return account!.pleroma.get('also_known_as');
}
}) as ImmutableList<string>;
const searchAccountIds = useAppSelector((state) => state.aliases.suggestions.items);
const loaded = useAppSelector((state) => state.aliases.suggestions.loaded);

@ -4,7 +4,7 @@ import { Link, Redirect, Route, Switch, useHistory, useLocation } from 'react-ro
import LandingGradient from 'soapbox/components/landing-gradient';
import SiteLogo from 'soapbox/components/site-logo';
import { useAppSelector, useFeatures, useSoapboxConfig, useOwnAccount } from 'soapbox/hooks';
import { useAppSelector, useFeatures, useSoapboxConfig, useOwnAccount, useInstance } from 'soapbox/hooks';
import { Button, Card, CardBody } from '../../components/ui';
import LoginPage from '../auth-login/components/login-page';
@ -27,12 +27,11 @@ const AuthLayout = () => {
const { search } = useLocation();
const account = useOwnAccount();
const siteTitle = useAppSelector(state => state.instance.title);
const instance = useInstance();
const features = useFeatures();
const soapboxConfig = useSoapboxConfig();
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
const features = useFeatures();
const instance = useAppSelector((state) => state.instance);
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
const isOpen = features.accountCreation && instance.registrations;
const pepeOpen = useAppSelector(state => state.verification.instance.get('registrations') === true);
const isLoginPage = history.location.pathname === '/login';
@ -47,7 +46,7 @@ const AuthLayout = () => {
<header className='flex justify-between relative py-12 px-2 mb-auto'>
<div className='relative z-0 flex-1 px-2 lg:flex lg:items-center lg:justify-center lg:absolute lg:inset-0'>
<Link to='/' className='cursor-pointer'>
<SiteLogo alt={siteTitle} className='h-7' />
<SiteLogo alt={instance.title} className='h-7' />
</Link>
</div>

@ -3,7 +3,7 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Card, HStack, Text } from 'soapbox/components/ui';
import { useAppSelector } from 'soapbox/hooks';
import { useInstance } from 'soapbox/hooks';
import ConsumerButton from './consumer-button';
@ -12,7 +12,8 @@ interface IConsumersList {
/** Displays OAuth consumers to log in with. */
const ConsumersList: React.FC<IConsumersList> = () => {
const providers = useAppSelector(state => ImmutableList<string>(state.instance.pleroma.get('oauth_consumer_strategies')));
const instance = useInstance();
const providers = ImmutableList<string>(instance.pleroma.get('oauth_consumer_strategies'));
if (providers.size > 0) {
return (

@ -12,7 +12,7 @@ import { openModal } from 'soapbox/actions/modals';
import BirthdayInput from 'soapbox/components/birthday-input';
import { Checkbox, Form, FormGroup, FormActions, Button, Input, Textarea } from 'soapbox/components/ui';
import CaptchaField from 'soapbox/features/auth-login/components/captcha';
import { useAppSelector, useAppDispatch, useSettings, useFeatures } from 'soapbox/hooks';
import { useAppDispatch, useSettings, useFeatures, useInstance } from 'soapbox/hooks';
const messages = defineMessages({
username: { id: 'registration.fields.username_placeholder', defaultMessage: 'Username' },
@ -42,7 +42,7 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
const settings = useSettings();
const features = useFeatures();
const instance = useAppSelector(state => state.instance);
const instance = useInstance();
const locale = settings.get('locale');
const needsConfirmation = !!instance.pleroma.getIn(['metadata', 'account_activation_required']);

@ -17,7 +17,7 @@ import AutosuggestInput, { AutoSuggestion } from 'soapbox/components/autosuggest
import AutosuggestTextarea from 'soapbox/components/autosuggest-textarea';
import Icon from 'soapbox/components/icon';
import { Button, HStack, Stack } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector, useCompose, useFeatures, usePrevious } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useCompose, useFeatures, useInstance, usePrevious } from 'soapbox/hooks';
import { isMobile } from 'soapbox/is-mobile';
import QuotedStatusContainer from '../containers/quoted-status-container';
@ -67,11 +67,12 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
const history = useHistory();
const intl = useIntl();
const dispatch = useAppDispatch();
const { configuration } = useInstance();
const compose = useCompose(id);
const showSearch = useAppSelector((state) => state.search.submitted && !state.search.hidden);
const isModalOpen = useAppSelector((state) => !!(state.modals.size && state.modals.last()!.modalType === 'COMPOSE'));
const maxTootChars = useAppSelector((state) => state.instance.getIn(['configuration', 'statuses', 'max_characters'])) as number;
const maxTootChars = configuration.getIn(['statuses', 'max_characters']) as number;
const scheduledStatusCount = useAppSelector((state) => state.get('scheduled_statuses').size);
const features = useFeatures();

@ -4,10 +4,11 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { addPollOption, changePollOption, changePollSettings, clearComposeSuggestions, fetchComposeSuggestions, removePoll, removePollOption, selectComposeSuggestion } from 'soapbox/actions/compose';
import AutosuggestInput from 'soapbox/components/autosuggest-input';
import { Button, Divider, HStack, Stack, Text, Toggle } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector, useCompose } from 'soapbox/hooks';
import { useAppDispatch, useCompose, useInstance } from 'soapbox/hooks';
import DurationSelector from './duration-selector';
import type { Map as ImmutableMap } from 'immutable';
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
const messages = defineMessages({
@ -110,16 +111,17 @@ interface IPollForm {
const PollForm: React.FC<IPollForm> = ({ composeId }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const { configuration } = useInstance();
const compose = useCompose(composeId);
const pollLimits = useAppSelector((state) => state.instance.getIn(['configuration', 'polls']) as any);
const pollLimits = configuration.get('polls') as ImmutableMap<string, number>;
const options = compose.poll?.options;
const expiresIn = compose.poll?.expires_in;
const isMultiple = compose.poll?.multiple;
const maxOptions = pollLimits.get('max_options');
const maxOptionChars = pollLimits.get('max_characters_per_option');
const maxOptions = pollLimits.get('max_options') as number;
const maxOptionChars = pollLimits.get('max_characters_per_option') as number;
const onRemoveOption = (index: number) => dispatch(removePollOption(composeId, index));
const onChangeOption = (index: number, title: string) => dispatch(changePollOption(composeId, index, title));

@ -3,10 +3,9 @@ import { FormattedList, FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux';
import { openModal } from 'soapbox/actions/modals';
import { useAppSelector, useCompose } from 'soapbox/hooks';
import { useAppSelector, useCompose, useFeatures } from 'soapbox/hooks';
import { statusToMentionsAccountIdsArray } from 'soapbox/reducers/compose';
import { makeGetStatus } from 'soapbox/selectors';
import { getFeatures } from 'soapbox/utils/features';
import type { Status as StatusEntity } from 'soapbox/types/entities';
@ -16,18 +15,15 @@ interface IReplyMentions {
const ReplyMentions: React.FC<IReplyMentions> = ({ composeId }) => {
const dispatch = useDispatch();
const getStatus = useCallback(makeGetStatus(), []);
const features = useFeatures();
const compose = useCompose(composeId);
const instance = useAppSelector((state) => state.instance);
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 { explicitAddressing } = getFeatures(instance);
if (!explicitAddressing || !status || !to) {
if (!features.explicitAddressing || !status || !to) {
return null;
}

@ -2,7 +2,7 @@ import React, { useRef } from 'react';
import { defineMessages, IntlShape, useIntl } from 'react-intl';
import { IconButton } from 'soapbox/components/ui';
import { useAppSelector } from 'soapbox/hooks';
import { useInstance } from 'soapbox/hooks';
import type { List as ImmutableList } from 'immutable';
@ -29,9 +29,10 @@ const UploadButton: React.FC<IUploadButton> = ({
resetFileKey,
}) => {
const intl = useIntl();
const { configuration } = useInstance();
const fileElement = useRef<HTMLInputElement>(null);
const attachmentTypes = useAppSelector(state => state.instance.configuration.getIn(['media_attachments', 'supported_mime_types']) as ImmutableList<string>);
const attachmentTypes = configuration.getIn(['media_attachments', 'supported_mime_types']) as ImmutableList<string>;
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
if (e.target.files?.length) {

@ -10,7 +10,7 @@ import { openModal } from 'soapbox/actions/modals';
import Blurhash from 'soapbox/components/blurhash';
import Icon from 'soapbox/components/icon';
import IconButton from 'soapbox/components/icon-button';
import { useAppDispatch, useAppSelector, useCompose } from 'soapbox/hooks';
import { useAppDispatch, useCompose, useInstance } from 'soapbox/hooks';
import Motion from '../../ui/util/optional-motion';
@ -70,9 +70,9 @@ const Upload: React.FC<IUpload> = ({ composeId, id }) => {
const intl = useIntl();
const history = useHistory();
const dispatch = useAppDispatch();
const { description_limit: descriptionLimit } = useInstance();
const media = useCompose(composeId).media_attachments.find(item => item.get('id') === id)!;
const descriptionLimit = useAppSelector((state) => state.instance.get('description_limit'));
const media = useCompose(composeId).media_attachments.find(item => item.id === id)!;
const [hovered, setHovered] = useState(false);
const [focused, setFocused] = useState(false);

@ -3,7 +3,7 @@ import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { Text, Widget } from 'soapbox/components/ui';
import { useAppSelector, useSoapboxConfig } from 'soapbox/hooks';
import { useInstance, useSoapboxConfig } from 'soapbox/hooks';
import SiteWallet from './site-wallet';
@ -18,9 +18,9 @@ interface ICryptoDonatePanel {
const CryptoDonatePanel: React.FC<ICryptoDonatePanel> = ({ limit = 3 }): JSX.Element | null => {
const intl = useIntl();
const history = useHistory();
const instance = useInstance();
const addresses = useSoapboxConfig().get('cryptoAddresses');
const siteTitle = useAppSelector((state) => state.instance.title);
if (limit === 0 || addresses.size === 0) {
return null;
@ -40,7 +40,7 @@ const CryptoDonatePanel: React.FC<ICryptoDonatePanel> = ({ limit = 3 }): JSX.Ele
<FormattedMessage
id='crypto_donate_panel.intro.message'
defaultMessage='{siteTitle} accepts cryptocurrency donations to fund our service. Thank you for your support!'
values={{ siteTitle }}
values={{ siteTitle: instance.title }}
/>
</Text>

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { Accordion, Column, Stack } from 'soapbox/components/ui';
import { useAppSelector } from 'soapbox/hooks';
import { useInstance } from 'soapbox/hooks';
import SiteWallet from './components/site-wallet';
@ -11,9 +11,10 @@ const messages = defineMessages({
});
const CryptoDonate: React.FC = (): JSX.Element => {
const [explanationBoxExpanded, toggleExplanationBox] = useState(true);
const siteTitle = useAppSelector((state) => state.instance.title);
const intl = useIntl();
const instance = useInstance();
const [explanationBoxExpanded, toggleExplanationBox] = useState(true);
return (
<Column label={intl.formatMessage(messages.heading)} withHeader>
@ -26,7 +27,7 @@ const CryptoDonate: React.FC = (): JSX.Element => {
<FormattedMessage
id='crypto_donate.explanation_box.message'
defaultMessage='{siteTitle} accepts cryptocurrency donations. You may send a donation to any of the addresses below. Thank you for your support!'
values={{ siteTitle }}
values={{ siteTitle: instance.title }}
/>
</Accordion>

@ -7,8 +7,7 @@ import { useLocation } from 'react-router-dom';
import { fetchDirectory, expandDirectory } from 'soapbox/actions/directory';
import LoadMore from 'soapbox/components/load-more';
import { Column, RadioButton, Stack, Text } from 'soapbox/components/ui';
import { useAppSelector } from 'soapbox/hooks';
import { getFeatures } from 'soapbox/utils/features';
import { useAppSelector, useFeatures, useInstance } from 'soapbox/hooks';
import AccountCard from './components/account-card';
@ -25,11 +24,11 @@ const Directory = () => {
const dispatch = useDispatch();
const { search } = useLocation();
const params = new URLSearchParams(search);
const instance = useInstance();
const features = useFeatures();
const accountIds = useAppSelector((state) => state.user_lists.directory.items);
const isLoading = useAppSelector((state) => state.user_lists.directory.isLoading);
const title = useAppSelector((state) => state.instance.get('title'));
const features = useAppSelector((state) => getFeatures(state.instance));
const [order, setOrder] = useState(params.get('order') || 'active');
const [local, setLocal] = useState(!!params.get('local'));
@ -71,7 +70,7 @@ const Directory = () => {
<fieldset className='mt-3'>
<legend className='sr-only'>Fediverse filter</legend>
<div className='space-y-2'>
<RadioButton name='local' value='1' label={intl.formatMessage(messages.local, { domain: title })} checked={local} onChange={handleChangeLocal} />
<RadioButton name='local' value='1' label={intl.formatMessage(messages.local, { domain: instance.title })} checked={local} onChange={handleChangeLocal} />
<RadioButton name='local' value='0' label={intl.formatMessage(messages.federated)} checked={!local} onChange={handleChangeLocal} />
</div>
</fieldset>

@ -19,7 +19,7 @@ import {
Textarea,
Toggle,
} from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch, useOwnAccount, useFeatures } from 'soapbox/hooks';
import { useAppDispatch, useOwnAccount, useFeatures, useInstance } from 'soapbox/hooks';
import { normalizeAccount } from 'soapbox/normalizers';
import resizeImage from 'soapbox/utils/resize-image';
@ -171,10 +171,11 @@ const ProfileField: StreamfieldComponent<AccountCredentialsField> = ({ value, on
const EditProfile: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const instance = useInstance();
const account = useOwnAccount();
const features = useFeatures();
const maxFields = useAppSelector(state => state.instance.pleroma.getIn(['metadata', 'fields_limits', 'max_fields'], 4) as number);
const maxFields = instance.pleroma.getIn(['metadata', 'fields_limits', 'max_fields'], 4) as number;
const [isLoading, setLoading] = useState(false);
const [data, setData] = useState<AccountCredentials>({});

@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl';
import Icon from 'soapbox/components/icon';
import { HStack, Stack, Text } from 'soapbox/components/ui';
import { useAppSelector } from 'soapbox/hooks';
import { useInstance } from 'soapbox/hooks';
import type { Map as ImmutableMap } from 'immutable';
@ -38,7 +38,7 @@ interface IInstanceRestrictions {
}
const InstanceRestrictions: React.FC<IInstanceRestrictions> = ({ remoteInstance }) => {
const instance = useAppSelector(state => state.instance);
const instance = useInstance();
const renderRestrictions = () => {
const items = [];

@ -1,9 +1,9 @@
import React, { useState } from 'react';
import React, { useState, useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import ScrollableList from 'soapbox/components/scrollable-list';
import { Accordion } from 'soapbox/components/ui';
import { useAppSelector } from 'soapbox/hooks';
import { useAppSelector, useInstance } from 'soapbox/hooks';
import { makeGetHosts } from 'soapbox/selectors';
import { federationRestrictionsDisclosed } from 'soapbox/utils/state';
@ -21,12 +21,12 @@ const messages = defineMessages({
notDisclosed: { id: 'federation_restrictions.not_disclosed_message', defaultMessage: '{siteTitle} does not disclose federation restrictions through the API.' },
});
const getHosts = makeGetHosts();
const FederationRestrictions = () => {
const intl = useIntl();
const instance = useInstance();
const getHosts = useCallback(makeGetHosts(), []);
const siteTitle = useAppSelector((state) => state.instance.get('title'));
const hosts = useAppSelector((state) => getHosts(state)) as ImmutableOrderedSet<string>;
const disclosed = useAppSelector((state) => federationRestrictionsDisclosed(state));
@ -45,11 +45,11 @@ const FederationRestrictions = () => {
expanded={explanationBoxExpanded}
onToggle={toggleExplanationBox}
>
{intl.formatMessage(messages.boxMessage, { siteTitle })}
{intl.formatMessage(messages.boxMessage, { siteTitle: instance.title })}
</Accordion>
<div className='pt-4'>
<ScrollableList emptyMessage={intl.formatMessage(emptyMessage, { siteTitle })}>
<ScrollableList emptyMessage={intl.formatMessage(emptyMessage, { siteTitle: instance.title })}>
{hosts.map((host) => <RestrictedInstance key={host} host={host} />)}
</ScrollableList>
</div>

@ -8,7 +8,7 @@ import { expandHomeTimeline } from 'soapbox/actions/timelines';
import PullToRefresh from 'soapbox/components/pull-to-refresh';
import { Column, Stack, Text } from 'soapbox/components/ui';
import Timeline from 'soapbox/features/ui/components/timeline';
import { useAppSelector, useAppDispatch, useFeatures } from 'soapbox/hooks';
import { useAppSelector, useAppDispatch, useFeatures, useInstance } from 'soapbox/hooks';
import { clearFeedAccountId } from '../../actions/timelines';
@ -20,12 +20,12 @@ const HomeTimeline: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const features = useFeatures();
const instance = useInstance();
const polling = useRef<NodeJS.Timer | null>(null);
const isPartial = useAppSelector(state => state.timelines.get('home')?.isPartial === true);
const currentAccountId = useAppSelector(state => state.timelines.get('home')?.feedAccountId as string | undefined);
const siteTitle = useAppSelector(state => state.instance.title);
const currentAccountRelationship = useAppSelector(state => currentAccountId ? state.relationships.get(currentAccountId) : null);
const handleLoadMore = (maxId: string) => {
@ -104,7 +104,7 @@ const HomeTimeline: React.FC = () => {
<FormattedMessage
id='empty_column.home.subtitle'
defaultMessage='{siteTitle} gets more interesting once you follow other users.'
values={{ siteTitle }}
values={{ siteTitle: instance.title }}
/>
</Text>
@ -116,7 +116,7 @@ const HomeTimeline: React.FC = () => {
values={{
public: (
<Link to='/timeline/local' className='text-primary-600 dark:text-primary-400 hover:underline'>
<FormattedMessage id='empty_column.home.local_tab' defaultMessage='the {site_title} tab' values={{ site_title: siteTitle }} />
<FormattedMessage id='empty_column.home.local_tab' defaultMessage='the {site_title} tab' values={{ site_title: instance.title }} />
</Link>
),
}}

@ -6,7 +6,7 @@ import Markup from 'soapbox/components/markup';
import { Button, Card, CardBody, Stack, Text } from 'soapbox/components/ui';
import VerificationBadge from 'soapbox/components/verification-badge';
import RegistrationForm from 'soapbox/features/auth-login/components/registration-form';
import { useAppDispatch, useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useFeatures, useInstance, useSoapboxConfig } from 'soapbox/hooks';
import { capitalize } from 'soapbox/utils/strings';
const LandingPage = () => {
@ -15,7 +15,7 @@ const LandingPage = () => {
const soapboxConfig = useSoapboxConfig();
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
const instance = useAppSelector((state) => state.instance);
const instance = useInstance();
const pepeOpen = useAppSelector(state => state.verification.instance.get('registrations') === true);
/** Registrations are closed */

@ -5,7 +5,7 @@ import { Link } from 'react-router-dom';
import { moveAccount } from 'soapbox/actions/security';
import snackbar from 'soapbox/actions/snackbar';
import { Button, Column, Form, FormActions, FormGroup, Input, Text } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { useAppDispatch, useInstance } from 'soapbox/hooks';
const messages = defineMessages({
heading: { id: 'column.migration', defaultMessage: 'Account migration' },
@ -21,8 +21,9 @@ const messages = defineMessages({
const Migration = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const instance = useInstance();
const cooldownPeriod = useAppSelector((state) => state.instance.pleroma.getIn(['metadata', 'migration_cooldown_period'])) as number | undefined;
const cooldownPeriod = instance.pleroma.getIn(['metadata', 'migration_cooldown_period']) as number | undefined;
const [targetAccount, setTargetAccount] = useState('');
const [password, setPassword] = useState('');

@ -12,7 +12,7 @@ import Icon from 'soapbox/components/icon';
import { HStack, Text, Emoji } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account-container';
import StatusContainer from 'soapbox/containers/status-container';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks';
import { makeGetNotification } from 'soapbox/selectors';
import { NotificationType, validType } from 'soapbox/utils/notification';
@ -157,7 +157,7 @@ const Notification: React.FC<INotificaton> = (props) => {
const history = useHistory();
const intl = useIntl();
const instance = useAppSelector((state) => state.instance);
const instance = useInstance();
const type = notification.type;
const { account, status } = notification;

@ -3,13 +3,13 @@ import { FormattedMessage } from 'react-intl';
import Account from 'soapbox/components/account';
import { Button, Card, CardBody, Icon, Stack, Text } from 'soapbox/components/ui';
import { useAppSelector, useOwnAccount } from 'soapbox/hooks';
import { useInstance, useOwnAccount } from 'soapbox/hooks';
import type { Account as AccountEntity } from 'soapbox/types/entities';
const FediverseStep = ({ onNext }: { onNext: () => void }) => {
const siteTitle = useAppSelector((state) => state.instance.title);
const account = useOwnAccount() as AccountEntity;
const instance = useInstance();
return (
<Card variant='rounded' size='xl'>
@ -22,7 +22,7 @@ const FediverseStep = ({ onNext }: { onNext: () => void }) => {
id='onboarding.fediverse.title'
defaultMessage='{siteTitle} is just one part of the Fediverse'
values={{
siteTitle,
siteTitle: instance.title,
}}
/>
</Text>
@ -35,7 +35,7 @@ const FediverseStep = ({ onNext }: { onNext: () => void }) => {
id='onboarding.fediverse.message'
defaultMessage='The Fediverse is a social network made up of thousands of diverse and independently-run social media sites (aka "servers"). You can follow users — and like, repost, and reply to posts — from most other Fediverse servers, because they can communicate with {siteTitle}.'
values={{
siteTitle,
siteTitle: instance.title,
}}
/>
</Text>

@ -8,7 +8,7 @@ import { fetchInstance } from 'soapbox/actions/instance';
import { openModal } from 'soapbox/actions/modals';
import SiteLogo from 'soapbox/components/site-logo';
import { Button, Form, HStack, IconButton, Input, Tooltip } from 'soapbox/components/ui';
import { useAppSelector, useFeatures, useSoapboxConfig, useOwnAccount } from 'soapbox/hooks';
import { useAppSelector, useFeatures, useSoapboxConfig, useOwnAccount, useInstance } from 'soapbox/hooks';
import Sonar from './sonar';
@ -34,7 +34,7 @@ const Header = () => {
const { links } = soapboxConfig;
const features = useFeatures();
const instance = useAppSelector((state) => state.instance);
const instance = useInstance();
const isOpen = features.accountCreation && instance.registrations;
const pepeOpen = useAppSelector(state => state.verification.instance.get('registrations') === true);

@ -8,7 +8,7 @@ import { expandPublicTimeline } from 'soapbox/actions/timelines';
import PullToRefresh from 'soapbox/components/pull-to-refresh';
import SubNavigation from 'soapbox/components/sub-navigation';
import { Accordion, Column } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks';
import { useAppDispatch, useInstance, useSettings } from 'soapbox/hooks';
import PinnedHostsPicker from '../remote-timeline/components/pinned-hosts-picker';
import Timeline from '../ui/components/timeline';
@ -22,12 +22,12 @@ const CommunityTimeline = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const instance = useInstance();
const settings = useSettings();
const onlyMedia = settings.getIn(['public', 'other', 'onlyMedia']);
const timelineId = 'public';
const siteTitle = useAppSelector((state) => state.instance.title);
const explanationBoxExpanded = settings.get('explanationBox');
const showExplanationBox = settings.get('showExplanationBox');
@ -79,13 +79,13 @@ const CommunityTimeline = () => {
id='fediverse_tab.explanation_box.explanation'
defaultMessage='{site_title} is part of the Fediverse, a social network made up of thousands of independent social media sites (aka "servers"). The posts you see here are from 3rd-party servers. You have the freedom to engage with them, or to block any server you don&apos;t like. Pay attention to the full username after the second @ symbol to know which server a post is from. To see only {site_title} posts, visit {local}.'
values={{
site_title: siteTitle,
site_title: instance.title,
local: (
<Link to='/timeline/local'>
<FormattedMessage
id='empty_column.home.local_tab'
defaultMessage='the {site_title} tab'
values={{ site_title: siteTitle }}
values={{ site_title: instance.title }}
/>
</Link>
),

@ -4,7 +4,7 @@ import { useParams } from 'react-router-dom';
import { Stack, CardTitle, Text } from 'soapbox/components/ui';
import RegistrationForm from 'soapbox/features/auth-login/components/registration-form';
import { useAppSelector } from 'soapbox/hooks';
import { useInstance } from 'soapbox/hooks';
interface RegisterInviteParams {
token: string,
@ -12,14 +12,14 @@ interface RegisterInviteParams {
/** Page to register with an invitation. */
const RegisterInvite: React.FC = () => {
const instance = useInstance();
const { token } = useParams<RegisterInviteParams>();
const siteTitle = useAppSelector(state => state.instance.title);
const title = (
<FormattedMessage
id='register_invite.title'
defaultMessage="You've been invited to join {siteTitle}!"
values={{ siteTitle }}
values={{ siteTitle: instance.title }}
/>
);

@ -2,7 +2,7 @@ import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Column, Divider, Stack, Text } from 'soapbox/components/ui';
import { useAppSelector } from 'soapbox/hooks';
import { useInstance } from 'soapbox/hooks';
import LinkFooter from '../ui/components/link-footer';
import PromoPanel from '../ui/components/promo-panel';
@ -13,7 +13,7 @@ const messages = defineMessages({
const ServerInfo = () => {
const intl = useIntl();
const instance = useAppSelector((state) => state.instance);
const instance = useInstance();
return (
<Column label={intl.formatMessage(messages.heading)}>

@ -6,8 +6,7 @@ import { useHistory } from 'react-router-dom';
import { fetchMfa } from 'soapbox/actions/mfa';
import List, { ListItem } from 'soapbox/components/list';
import { Card, CardBody, CardHeader, CardTitle, Column } from 'soapbox/components/ui';
import { useAppSelector, useOwnAccount } from 'soapbox/hooks';
import { getFeatures } from 'soapbox/utils/features';
import { useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks';
import Preferences from '../preferences';
@ -36,7 +35,7 @@ const Settings = () => {
const intl = useIntl();
const mfa = useAppSelector((state) => state.security.get('mfa'));
const features = useAppSelector((state) => getFeatures(state.instance));
const features = useFeatures();
const account = useOwnAccount();
const navigateToChangeEmail = () => history.push('/settings/email');

@ -52,6 +52,8 @@ const messages = defineMessages({
singleUserModeHint: { id: 'soapbox_config.single_user_mode_hint', defaultMessage: 'Front page will redirect to a given user profile.' },
singleUserModeProfileLabel: { id: 'soapbox_config.single_user_mode_profile_label', defaultMessage: 'Main user handle' },
singleUserModeProfileHint: { id: 'soapbox_config.single_user_mode_profile_hint', defaultMessage: '@handle' },
feedInjectionLabel: { id: 'soapbox_config.feed_injection_label', defaultMessage: 'Feed injection' },
feedInjectionHint: { id: 'soapbox_config.feed_injection_hint', defaultMessage: 'Inject the feed with additional content, such as suggested profiles.' },
});
type ValueGetter<T = Element> = (e: React.ChangeEvent<T>) => any;
@ -261,6 +263,16 @@ const SoapboxConfig: React.FC = () => {
/>
</ListItem>
<ListItem
label={intl.formatMessage(messages.feedInjectionLabel)}
hint={intl.formatMessage(messages.feedInjectionHint)}
>
<Toggle
checked={soapbox.feedInjection === true}
onChange={handleChange(['feedInjection'], (e) => e.target.checked)}
/>
</ListItem>
<ListItem label={intl.formatMessage(messages.displayCtaLabel)}>
<Toggle
checked={soapbox.displayCta === true}

@ -127,7 +127,7 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
</Stack>
</Stack>
<HStack justifyContent='between' alignItems='center' className='py-2' wrap>
<HStack justifyContent='between' alignItems='center' className='py-3' wrap>
<StatusInteractionBar status={actualStatus} />
<HStack space={1} alignItems='center'>

@ -1,11 +1,11 @@
import classNames from 'clsx';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { List as ImmutableList } from 'immutable';
import React from 'react';
import { FormattedNumber } from 'react-intl';
import { FormattedMessage, FormattedNumber } from 'react-intl';
import { useDispatch } from 'react-redux';
import { openModal } from 'soapbox/actions/modals';
import { HStack, IconButton, Text, Emoji } from 'soapbox/components/ui';
import { HStack, Text, Emoji } from 'soapbox/components/ui';
import { useAppSelector, useSoapboxConfig, useFeatures } from 'soapbox/hooks';
import { reduceEmoji } from 'soapbox/utils/emoji-reacts';
@ -42,11 +42,10 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
}));
};
const onOpenReactionsModal = (username: string, statusId: string, reaction: string): void => {
const onOpenReactionsModal = (username: string, statusId: string): void => {
dispatch(openModal('REACTIONS', {
username,
statusId,
reaction,
}));
};
@ -56,7 +55,7 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
status.favourites_count,
status.favourited,
allowedEmoji,
).reverse();
);
};
const handleOpenReblogsModal: React.EventHandler<React.MouseEvent> = (e) => {
@ -69,22 +68,17 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
const getReposts = () => {
if (status.reblogs_count) {
return (
<HStack space={0.5} alignItems='center'>
<IconButton
className='text-success-600 cursor-pointer'
src={require('@tabler/icons/repeat.svg')}
role='presentation'
onClick={handleOpenReblogsModal}
<InteractionCounter count={status.reblogs_count} onClick={handleOpenReblogsModal}>
<FormattedMessage
id='status.interactions.reblogs'
defaultMessage='{count, plural, one {Repost} other {Reposts}}'
values={{ count: status.reblogs_count }}
/>
<Text theme='muted' size='sm'>
<FormattedNumber value={status.reblogs_count} />
</Text>
</HStack>
</InteractionCounter>
);
}
return '';
return null;
};
const handleOpenFavouritesModal: React.EventHandler<React.MouseEvent<HTMLButtonElement>> = (e) => {
@ -97,31 +91,25 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
const getFavourites = () => {
if (status.favourites_count) {
return (
<HStack space={0.5} alignItems='center'>
<IconButton
className={classNames({
'text-accent-300': true,
'cursor-default': !features.exposableReactions,
})}
src={require('@tabler/icons/heart.svg')}
iconClassName='fill-accent-300'
role='presentation'
onClick={features.exposableReactions ? handleOpenFavouritesModal : undefined}
<InteractionCounter count={status.favourites_count} onClick={features.exposableReactions ? handleOpenFavouritesModal : undefined}>
<FormattedMessage
id='status.interactions.favourites'
defaultMessage='{count, plural, one {Like} other {Likes}}'
values={{ count: status.favourites_count }}
/>
<Text theme='muted' size='sm'>
<FormattedNumber value={status.favourites_count} />
</Text>
</HStack>
</InteractionCounter>
);
}
return '';
return null;
};
const handleOpenReactionsModal = (reaction: ImmutableMap<string, any>) => () => {
if (!me) onOpenUnauthorizedModal();
else onOpenReactionsModal(account.acct, status.id, String(reaction.get('name')));
const handleOpenReactionsModal = () => {
if (!me) {
return onOpenUnauthorizedModal();
}
onOpenReactionsModal(account.acct, status.id);
};
const getEmojiReacts = () => {
@ -130,43 +118,67 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
acc + cur.get('count')
), 0);
if (count > 0) {
if (count) {
return (
<HStack space={0.5} className='emoji-reacts-container' alignItems='center'>
<div className='emoji-reacts'>
{emojiReacts.map((e, i) => {
<InteractionCounter count={count} onClick={features.exposableReactions ? handleOpenReactionsModal : undefined}>
<HStack space={0.5} alignItems='center'>
{emojiReacts.take(3).map((e, i) => {
return (
<HStack space={0.5} className='emoji-react p-1' alignItems='center' key={i}>
<Emoji
className={classNames('emoji-react__emoji w-5 h-5 flex-none', { 'cursor-pointer': features.exposableReactions })}
emoji={e.get('name')}
onClick={features.exposableReactions ? handleOpenReactionsModal(e) : undefined}
/>
<Text theme='muted' size='sm' className='emoji-react__count'>
<FormattedNumber value={e.get('count')} />
</Text>
</HStack>
<Emoji
key={i}
className='w-4.5 h-4.5 flex-none'
emoji={e.get('name')}
/>
);
})}
</div>
<Text theme='muted' size='sm' className='emoji-reacts__count'>
<FormattedNumber value={count} />
</Text>
</HStack>
</HStack>
</InteractionCounter>
);
}
return '';
return null;
};
return (
<HStack space={3}>
{getReposts()}
{features.emojiReacts ? getEmojiReacts() : getFavourites()}
</HStack>
);
};
interface IInteractionCounter {
count: number,
onClick?: React.MouseEventHandler<HTMLButtonElement>,
children: React.ReactNode,
}
const InteractionCounter: React.FC<IInteractionCounter> = ({ count, onClick, children }) => {
const features = useFeatures();
return (
<button
type='button'
onClick={onClick}
className={
classNames({
'text-gray-600 dark:text-gray-700': true,
'hover:underline': features.exposableReactions,
'cursor-default': !features.exposableReactions,
})
}
>
<HStack space={1} alignItems='center'>
<Text theme='primary' weight='bold'>
<FormattedNumber value={count} />
</Text>
<Text tag='div' theme='muted'>
{children}
</Text>
</HStack>
</button>
);
};
export default StatusInteractionBar;

@ -2,12 +2,12 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Card, CardTitle, Text, Stack, Button } from 'soapbox/components/ui';
import { useAppSelector, useSoapboxConfig } from 'soapbox/hooks';
import { useInstance, useSoapboxConfig } from 'soapbox/hooks';
/** Prompts logged-out users to log in when viewing a thread. */
const ThreadLoginCta: React.FC = () => {
const instance = useInstance();
const { displayCta } = useSoapboxConfig();
const siteTitle = useAppSelector(state => state.instance.title);
if (!displayCta) return null;
@ -19,7 +19,7 @@ const ThreadLoginCta: React.FC = () => {
<FormattedMessage
id='thread_login.message'
defaultMessage='Join {siteTitle} to get the full story and details.'
values={{ siteTitle }}
values={{ siteTitle: instance.title }}
/>
</Text>
</Stack>

@ -2,11 +2,11 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Banner, Button, HStack, Stack, Text } from 'soapbox/components/ui';
import { useAppSelector, useSoapboxConfig } from 'soapbox/hooks';
import { useAppSelector, useInstance, useSoapboxConfig } from 'soapbox/hooks';
const CtaBanner = () => {
const instance = useInstance();
const { displayCta, singleUserMode } = useSoapboxConfig();
const siteTitle = useAppSelector((state) => state.instance.title);
const me = useAppSelector((state) => state.me);
if (me || !displayCta || singleUserMode) return null;
@ -17,7 +17,7 @@ const CtaBanner = () => {
<HStack alignItems='center' justifyContent='between'>
<Stack>
<Text theme='white' size='xl' weight='bold'>
<FormattedMessage id='signup_panel.title' defaultMessage='New to {site_title}?' values={{ site_title: siteTitle }} />
<FormattedMessage id='signup_panel.title' defaultMessage='New to {site_title}?' values={{ site_title: instance.title }} />
</Text>
<Text theme='white' weight='medium' className='opacity-90'>

@ -2,8 +2,7 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Modal } from 'soapbox/components/ui';
import { useAppSelector } from 'soapbox/hooks';
import { getFeatures } from 'soapbox/utils/features';
import { useFeatures } from 'soapbox/hooks';
interface IHotkeysModal {
onClose: () => void,
@ -22,7 +21,7 @@ const TableCell: React.FC<{ children: React.ReactNode }> = ({ children }) => (
);
const HotkeysModal: React.FC<IHotkeysModal> = ({ onClose }) => {
const features = useAppSelector((state) => getFeatures(state.instance));
const features = useFeatures();
return (
<Modal

@ -4,7 +4,7 @@ import { defineMessages, useIntl } from 'react-intl';
import SiteLogo from 'soapbox/components/site-logo';
import { Text, Button, Icon, Modal } from 'soapbox/components/ui';
import { useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks';
import { useAppSelector, useFeatures, useInstance, useSoapboxConfig } from 'soapbox/hooks';
const messages = defineMessages({
download: { id: 'landing_page_modal.download', defaultMessage: 'Download' },
@ -25,7 +25,7 @@ const LandingPageModal: React.FC<ILandingPageModal> = ({ onClose }) => {
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
const { links } = soapboxConfig;
const instance = useAppSelector((state) => state.instance);
const instance = useInstance();
const features = useFeatures();
const isOpen = features.accountCreation && instance.registrations;

@ -23,6 +23,8 @@ const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
submit: { id: 'report.submit', defaultMessage: 'Submit' },
cancel: { id: 'common.cancel', defaultMessage: 'Cancel' },
previous: { id: 'report.previous', defaultMessage: 'Previous' },
});
enum Steps {
@ -101,23 +103,6 @@ const ReportModal = ({ onClose }: IReportModal) => {
}
};
const handleNextStep = () => {
switch (currentStep) {
case Steps.ONE:
setCurrentStep(Steps.TWO);
break;
case Steps.TWO:
handleSubmit();
break;
case Steps.THREE:
dispatch(submitReportSuccess());
onClose();
break;
default:
break;
}
};
const renderSelectedStatuses = useCallback(() => {
switch (selectedStatusIds.size) {
case 0:
@ -131,6 +116,28 @@ const ReportModal = ({ onClose }: IReportModal) => {
}
}, [selectedStatusIds.size]);
const cancelText = useMemo(() => {
switch (currentStep) {
case Steps.ONE:
return intl.formatMessage(messages.cancel);
default:
return intl.formatMessage(messages.previous);
}
}, [currentStep]);
const cancelAction = () => {
switch (currentStep) {
case Steps.ONE:
onClose();
break;
case Steps.TWO:
setCurrentStep(Steps.ONE);
break;
default:
break;
}
};
const confirmationText = useMemo(() => {
switch (currentStep) {
case Steps.TWO:
@ -142,6 +149,23 @@ const ReportModal = ({ onClose }: IReportModal) => {
}
}, [currentStep]);
const handleNextStep = () => {
switch (currentStep) {
case Steps.ONE:
setCurrentStep(Steps.TWO);
break;
case Steps.TWO:
handleSubmit();
break;
case Steps.THREE:
dispatch(submitReportSuccess());
onClose();
break;
default:
break;
}
};
const isConfirmationButtonDisabled = useMemo(() => {
if (currentStep === Steps.THREE) {
return false;
@ -179,8 +203,8 @@ const ReportModal = ({ onClose }: IReportModal) => {
<Modal
title={<FormattedMessage id='report.target' defaultMessage='Reporting {target}' values={{ target: <strong>@{account.acct}</strong> }} />}
onClose={onClose}
cancelText={<FormattedMessage id='common.cancel' defaultMessage='Cancel' />}
cancelAction={currentStep === Steps.THREE ? undefined : onClose}
cancelText={cancelText}
cancelAction={cancelAction}
confirmationAction={handleNextStep}
confirmationText={confirmationText}
confirmationDisabled={isConfirmationButtonDisabled}

@ -5,7 +5,7 @@ import { useHistory } from 'react-router-dom';
import { remoteInteraction } from 'soapbox/actions/interactions';
import snackbar from 'soapbox/actions/snackbar';
import { Button, Modal, Stack, Text } from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch, useFeatures, useSoapboxConfig } from 'soapbox/hooks';
import { useAppSelector, useAppDispatch, useFeatures, useSoapboxConfig, useInstance } from 'soapbox/hooks';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
@ -29,9 +29,9 @@ const UnauthorizedModal: React.FC<IUnauthorizedModal> = ({ action, onClose, acco
const intl = useIntl();
const history = useHistory();
const dispatch = useAppDispatch();
const instance = useInstance();
const { singleUserMode } = useSoapboxConfig();
const siteTitle = useAppSelector(state => state.instance.title);
const username = useAppSelector(state => state.accounts.get(accountId)?.display_name);
const features = useFeatures();
@ -121,7 +121,7 @@ const UnauthorizedModal: React.FC<IUnauthorizedModal> = ({ action, onClose, acco
</div>
{!singleUserMode && (
<Text size='lg' weight='medium'>
<FormattedMessage id='unauthorized_modal.title' defaultMessage='Sign up for {site_title}' values={{ site_title: siteTitle }} />
<FormattedMessage id='unauthorized_modal.title' defaultMessage='Sign up for {site_title}' values={{ site_title: instance.title }} />
</Text>
)}
</div>
@ -135,7 +135,7 @@ const UnauthorizedModal: React.FC<IUnauthorizedModal> = ({ action, onClose, acco
return (
<Modal
title={<FormattedMessage id='unauthorized_modal.title' defaultMessage='Sign up for {site_title}' values={{ site_title: siteTitle }} />}
title={<FormattedMessage id='unauthorized_modal.title' defaultMessage='Sign up for {site_title}' values={{ site_title: instance.title }} />}
onClose={onClickClose}
confirmationAction={onLogin}
confirmationText={<FormattedMessage id='account.login' defaultMessage='Log in' />}

@ -7,7 +7,7 @@ import { closeModal } from 'soapbox/actions/modals';
import snackbar from 'soapbox/actions/snackbar';
import { reConfirmPhoneVerification, reRequestPhoneVerification } from 'soapbox/actions/verification';
import { FormGroup, PhoneInput, Modal, Stack, Text } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks';
import { getAccessToken } from 'soapbox/utils/auth';
const messages = defineMessages({
@ -56,8 +56,8 @@ enum Statuses {
const VerifySmsModal: React.FC<IVerifySmsModal> = ({ onClose }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const instance = useInstance();
const accessToken = useAppSelector((state) => getAccessToken(state));
const title = useAppSelector((state) => state.instance.title);
const isLoading = useAppSelector((state) => state.verification.isLoading);
const [status, setStatus] = useState<Statuses>(Statuses.IDLE);
@ -143,7 +143,7 @@ const VerifySmsModal: React.FC<IVerifySmsModal> = ({ onClose }) => {
id='sms_verification.modal.verify_help_text'
defaultMessage='Verify your phone number to start using {instance}.'
values={{
instance: title,
instance: instance.title,
}}
/>
</Text>

@ -2,11 +2,11 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Button, Stack, Text } from 'soapbox/components/ui';
import { useAppSelector, useSoapboxConfig } from 'soapbox/hooks';
import { useAppSelector, useInstance, useSoapboxConfig } from 'soapbox/hooks';
const SignUpPanel = () => {
const instance = useInstance();
const { singleUserMode } = useSoapboxConfig();
const siteTitle = useAppSelector((state) => state.instance.title);
const me = useAppSelector((state) => state.me);
if (me || singleUserMode) return null;
@ -15,7 +15,7 @@ const SignUpPanel = () => {
<Stack space={2}>
<Stack>
<Text size='lg' weight='bold'>
<FormattedMessage id='signup_panel.title' defaultMessage='New to {site_title}?' values={{ site_title: siteTitle }} />
<FormattedMessage id='signup_panel.title' defaultMessage='New to {site_title}?' values={{ site_title: instance.title }} />
</Text>
<Text theme='muted' size='sm'>

@ -2,20 +2,20 @@ import React from 'react';
import Icon from 'soapbox/components/icon';
import { Widget, Stack, Text } from 'soapbox/components/ui';
import { useAppSelector, useSettings, useSoapboxConfig } from 'soapbox/hooks';
import { useInstance, useSettings, useSoapboxConfig } from 'soapbox/hooks';
const PromoPanel: React.FC = () => {
const instance = useInstance();
const { promoPanel } = useSoapboxConfig();
const settings = useSettings();
const siteTitle = useAppSelector(state => state.instance.title);
const promoItems = promoPanel.get('items');
const locale = settings.get('locale');
if (!promoItems || promoItems.isEmpty()) return null;
return (
<Widget title={siteTitle}>
<Widget title={instance.title}>
<Stack space={2}>
{promoItems.map((item, i) => (
<Text key={i}>

@ -25,18 +25,15 @@ import Icon from 'soapbox/components/icon';
import SidebarNavigation from 'soapbox/components/sidebar-navigation';
import ThumbNavigation from 'soapbox/components/thumb-navigation';
import { Layout } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig, useFeatures } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig, useFeatures, useInstance } from 'soapbox/hooks';
import AdminPage from 'soapbox/pages/admin-page';
import DefaultPage from 'soapbox/pages/default-page';
// import GroupsPage from 'soapbox/pages/groups_page';
// import GroupPage from 'soapbox/pages/group_page';
import HomePage from 'soapbox/pages/home-page';
import ProfilePage from 'soapbox/pages/profile-page';
import RemoteInstancePage from 'soapbox/pages/remote-instance-page';
import StatusPage from 'soapbox/pages/status-page';
import { getAccessToken, getVapidKey } from 'soapbox/utils/auth';
import { isStandalone } from 'soapbox/utils/state';
// import GroupSidebarPanel from '../groups/sidebar_panel';
import BackgroundShapes from './components/background-shapes';
import Navbar from './components/navbar';
@ -190,18 +187,6 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {
<WrappedRoute path='/messages' page={DefaultPage} component={Conversations} content={children} />
)}
{/* Gab groups */}
{/*
<WrappedRoute path='/groups' exact page={GroupsPage} component={Groups} content={children} componentParams={{ activeTab: 'featured' }} />
<WrappedRoute path='/groups/create' page={GroupsPage} component={Groups} content={children} componentParams={{ showCreateForm: true, activeTab: 'featured' }} />
<WrappedRoute path='/groups/browse/member' page={GroupsPage} component={Groups} content={children} componentParams={{ activeTab: 'member' }} />
<WrappedRoute path='/groups/browse/admin' page={GroupsPage} component={Groups} content={children} componentParams={{ activeTab: 'admin' }} />
<WrappedRoute path='/groups/:id/members' page={GroupPage} component={GroupMembers} content={children} />
<WrappedRoute path='/groups/:id/removed_accounts' page={GroupPage} component={GroupRemovedAccounts} content={children} />
<WrappedRoute path='/groups/:id/edit' page={GroupPage} component={GroupEdit} content={children} />
<WrappedRoute path='/groups/:id' page={GroupPage} component={GroupTimeline} content={children} />
*/}
{/* Mastodon web routes */}
<Redirect from='/web/:path1/:path2/:path3' to='/:path1/:path2/:path3' />
<Redirect from='/web/:path1/:path2' to='/:path1/:path2' />
@ -331,6 +316,7 @@ const UI: React.FC = ({ children }) => {
const intl = useIntl();
const history = useHistory();
const dispatch = useAppDispatch();
const instance = useInstance();
const [draggingOver, setDraggingOver] = useState<boolean>(false);
const [mobile, setMobile] = useState<boolean>(isMobile(window.innerWidth));
@ -347,7 +333,7 @@ const UI: React.FC = ({ children }) => {
const dropdownMenuIsOpen = useAppSelector(state => state.dropdown_menu.openId !== null);
const accessToken = useAppSelector(state => getAccessToken(state));
const streamingUrl = useAppSelector(state => state.instance.urls.get('streaming_api'));
const streamingUrl = instance.urls.get('streaming_api');
const standalone = useAppSelector(isStandalone);
const handleDragEnter = (e: DragEvent) => {

@ -8,7 +8,7 @@ import { startOnboarding } from 'soapbox/actions/onboarding';
import snackbar from 'soapbox/actions/snackbar';
import { createAccount, removeStoredVerification } from 'soapbox/actions/verification';
import { Button, Form, FormGroup, Input, Text } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector, useSoapboxConfig } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useInstance, useSoapboxConfig } from 'soapbox/hooks';
import { getRedirectUrl } from 'soapbox/utils/redirect';
import PasswordIndicator from './components/password-indicator';
@ -32,11 +32,11 @@ const initialState = {
const Registration = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const instance = useInstance();
const soapboxConfig = useSoapboxConfig();
const { links } = soapboxConfig;
const isLoading = useAppSelector((state) => state.verification.isLoading as boolean);
const siteTitle = useAppSelector((state) => state.instance.title);
const [state, setState] = React.useState(initialState);
const [shouldRedirect, setShouldRedirect] = React.useState<boolean>(false);
@ -56,7 +56,7 @@ const Registration = () => {
dispatch(startOnboarding());
dispatch(
snackbar.success(
intl.formatMessage(messages.success, { siteTitle }),
intl.formatMessage(messages.success, { siteTitle: instance.title }),
),
);
})

@ -4,7 +4,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import snackbar from 'soapbox/actions/snackbar';
import { verifyAge } from 'soapbox/actions/verification';
import { Button, Datepicker, Form, Text } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks';
const messages = defineMessages({
fail: {
@ -24,10 +24,10 @@ function meetsAgeMinimum(birthday: Date, ageMinimum: number) {
const AgeVerification = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const instance = useInstance();
const isLoading = useAppSelector((state) => state.verification.isLoading) as boolean;
const ageMinimum = useAppSelector((state) => state.verification.ageMinimum) as any;
const siteTitle = useAppSelector((state) => state.instance.title);
const [date, setDate] = React.useState('');
const isValid = typeof date === 'object';
@ -65,7 +65,7 @@ const AgeVerification = () => {
id='age_verification.body'
defaultMessage='{siteTitle} requires users to be at least {ageMinimum} years old to access its platform. Anyone under the age of {ageMinimum} years old cannot access this platform.'
values={{
siteTitle,
siteTitle: instance.title,
ageMinimum,
}}
/>

@ -8,11 +8,11 @@ import { openModal } from 'soapbox/actions/modals';
import LandingGradient from 'soapbox/components/landing-gradient';
import SiteLogo from 'soapbox/components/site-logo';
import { Button, Stack, Text } from 'soapbox/components/ui';
import { useAppSelector, useOwnAccount } from 'soapbox/hooks';
import { useInstance, useOwnAccount } from 'soapbox/hooks';
const WaitlistPage = (/* { account } */) => {
const WaitlistPage = () => {
const dispatch = useDispatch();
const title = useAppSelector((state) => state.instance.title);
const instance = useInstance();
const me = useOwnAccount();
const isSmsVerified = me?.source.get('sms_verified');
@ -59,7 +59,7 @@ const WaitlistPage = (/* { account } */) => {
<FormattedMessage
id='waitlist.body'
defaultMessage='Welcome back to {title}! You were previously placed on our waitlist. Please verify your phone number to receive immediate access to your account!'
values={{ title }}
values={{ title: instance.title }}
/>
</Text>

@ -5,6 +5,7 @@ export { useAppSelector } from './useAppSelector';
export { useCompose } from './useCompose';
export { useDimensions } from './useDimensions';
export { useFeatures } from './useFeatures';
export { useInstance } from './useInstance';
export { useLocale } from './useLocale';
export { useOnScreen } from './useOnScreen';
export { useOwnAccount } from './useOwnAccount';

@ -1,9 +1,9 @@
import { useAppSelector } from 'soapbox/hooks';
import { getFeatures } from 'soapbox/utils/features';
import { getFeatures, Features } from 'soapbox/utils/features';
import type { Features } from 'soapbox/utils/features';
import { useInstance } from './useInstance';
/** Get features for the current instance */
/** Get features for the current instance. */
export const useFeatures = (): Features => {
return useAppSelector((state) => getFeatures(state.instance));
const instance = useInstance();
return getFeatures(instance);
};

@ -0,0 +1,6 @@
import { useAppSelector } from 'soapbox/hooks';
/** Get the Instance for the current backend. */
export const useInstance = () => {
return useAppSelector((state) => state.instance);
};

@ -1,13 +1,21 @@
import { useCallback } from 'react';
import { useAppSelector } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
import type { Account } from 'soapbox/types/entities';
// FIXME: There is no reason this selector shouldn't be global accross the whole app
// FIXME: getAccount() has the wrong type??
const getAccount: (state: any, accountId: any) => any = makeGetAccount();
/** Get the logged-in account from the store, if any */
/** Get the logged-in account from the store, if any. */
export const useOwnAccount = (): Account | null => {
return useAppSelector((state) => getAccount(state, state.me));
const getAccount = useCallback(makeGetAccount(), []);
return useAppSelector((state) => {
const { me } = state;
if (typeof me === 'string') {
return getAccount(state, me);
} else {
return null;
}
});
};

@ -994,6 +994,8 @@
"status.in_review_summary.summary": "This post has been sent to Moderation for review and is only visible to you.",
"status.in_review_summary.contact": "If you believe this is in error please {link}.",
"status.in_review_summary.link": "Contact Support",
"status.interactions.reblogs": "{count, plural, one {Repost} other {Reposts}}",
"status.interactions.favourites": "{count, plural, one {Like} other {Likes}}",
"status.load_more": "Load more",
"status.media_hidden": "Media hidden",
"status.mention": "Mention @{name}",

@ -112,6 +112,8 @@ export const SoapboxConfigRecord = ImmutableRecord({
linkFooterMessage: '',
links: ImmutableMap<string, string>(),
displayCta: true,
/** Whether to inject suggested profiles into the Home feed. */
feedInjection: true,
}, 'SoapboxConfig');
type SoapboxConfigMap = ImmutableMap<string, any>;

@ -42,6 +42,9 @@ module.exports = {
'mono',
],
},
spacing: {
'4.5': '1.125rem',
},
colors: parseColorMatrix({
// Define color matrix (of available colors)
// Colors are configured at runtime with CSS variables in soapbox.json

Loading…
Cancel
Save