Types, add max height to reactions modal

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-08-23 18:30:50 +02:00
parent ff338a06bf
commit c22898cebc
11 changed files with 115 additions and 95 deletions

View file

@ -1,9 +1,7 @@
import type { AnyAction } from 'redux';
const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT';
const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION';
const SET_ALERTS = 'PUSH_NOTIFICATIONS_SET_ALERTS';
const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT' as const;
const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION' as const;
const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION' as const;
const SET_ALERTS = 'PUSH_NOTIFICATIONS_SET_ALERTS' as const;
const setBrowserSupport = (value: boolean) => ({
type: SET_BROWSER_SUPPORT,
@ -19,13 +17,17 @@ const clearSubscription = () => ({
type: CLEAR_SUBSCRIPTION,
});
const setAlerts = (path: Array<string>, value: any) =>
(dispatch: React.Dispatch<AnyAction>) =>
dispatch({
type: SET_ALERTS,
path,
value,
});
const setAlerts = (path: Array<string>, value: any) => ({
type: SET_ALERTS,
path,
value,
});
type SetterAction =
| ReturnType<typeof setBrowserSupport>
| ReturnType<typeof setSubscription>
| ReturnType<typeof clearSubscription>
| ReturnType<typeof setAlerts>;
export {
SET_BROWSER_SUPPORT,
@ -36,4 +38,5 @@ export {
setSubscription,
clearSubscription,
setAlerts,
type SetterAction,
};

View file

@ -12,32 +12,22 @@ import type { APIEntity } from 'soapbox/types/entities';
const SOAPBOX_CONFIG_REQUEST_SUCCESS = 'SOAPBOX_CONFIG_REQUEST_SUCCESS' as const;
const SOAPBOX_CONFIG_REQUEST_FAIL = 'SOAPBOX_CONFIG_REQUEST_FAIL' as const;
const SOAPBOX_CONFIG_REMEMBER_REQUEST = 'SOAPBOX_CONFIG_REMEMBER_REQUEST' as const;
const SOAPBOX_CONFIG_REMEMBER_SUCCESS = 'SOAPBOX_CONFIG_REMEMBER_SUCCESS' as const;
const SOAPBOX_CONFIG_REMEMBER_FAIL = 'SOAPBOX_CONFIG_REMEMBER_FAIL' as const;
const getSoapboxConfig = createSelector([
(state: RootState) => state.soapbox,
(state: RootState) => state.auth.client.features,
], (soapbox, features) => {
// Do some additional normalization with the state
return normalizeSoapboxConfig(soapbox).withMutations(soapboxConfig => {
// If displayFqn isn't set, infer it from federation
if (soapbox.get('displayFqn') === undefined) {
soapboxConfig.set('displayFqn', features.federating);
}
});
return normalizeSoapboxConfig(soapbox);
});
const rememberSoapboxConfig = (host: string | null) =>
(dispatch: AppDispatch) => {
dispatch({ type: SOAPBOX_CONFIG_REMEMBER_REQUEST, host });
return KVStore.getItemOrError(`soapbox_config:${host}`).then(soapboxConfig => {
dispatch({ type: SOAPBOX_CONFIG_REMEMBER_SUCCESS, host, soapboxConfig });
return soapboxConfig;
}).catch(error => {
dispatch({ type: SOAPBOX_CONFIG_REMEMBER_FAIL, host, error, skipAlert: true });
});
}).catch(() => {});
};
const fetchFrontendConfigurations = () =>
@ -107,9 +97,7 @@ const isObject = (o: any) => o instanceof Object && o.constructor === Object;
export {
SOAPBOX_CONFIG_REQUEST_SUCCESS,
SOAPBOX_CONFIG_REQUEST_FAIL,
SOAPBOX_CONFIG_REMEMBER_REQUEST,
SOAPBOX_CONFIG_REMEMBER_SUCCESS,
SOAPBOX_CONFIG_REMEMBER_FAIL,
getSoapboxConfig,
rememberSoapboxConfig,
fetchFrontendConfigurations,

View file

@ -2,6 +2,7 @@ import { getClient } from '../api';
import { importFetchedStatuses } from './importer';
import type { Status as BaseStatus, PaginatedResponse } from 'pl-api';
import type { AppDispatch, RootState } from 'soapbox/store';
const STATUS_QUOTES_FETCH_REQUEST = 'STATUS_QUOTES_FETCH_REQUEST' as const;
@ -14,34 +15,70 @@ const STATUS_QUOTES_EXPAND_FAIL = 'STATUS_QUOTES_EXPAND_FAIL' as const;
const noOp = () => new Promise(f => f(null));
interface FetchStatusQuotesRequestAction {
type: typeof STATUS_QUOTES_FETCH_REQUEST;
statusId: string;
}
interface FetchStatusQuotesSuccessAction {
type: typeof STATUS_QUOTES_FETCH_SUCCESS;
statusId: string;
statuses: Array<BaseStatus>;
next: (() => Promise<PaginatedResponse<BaseStatus>>) | null;
}
interface FetchStatusQuotesFailAction {
type: typeof STATUS_QUOTES_FETCH_FAIL;
statusId: string;
error: unknown;
}
const fetchStatusQuotes = (statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (getState().status_lists.getIn([`quotes:${statusId}`, 'isLoading'])) {
return dispatch(noOp);
}
dispatch({
statusId,
type: STATUS_QUOTES_FETCH_REQUEST,
});
const action: FetchStatusQuotesRequestAction = { type: STATUS_QUOTES_FETCH_REQUEST, statusId };
dispatch(action);
return getClient(getState).statuses.getStatusQuotes(statusId).then(response => {
dispatch(importFetchedStatuses(response.items));
return dispatch({
const action: FetchStatusQuotesSuccessAction = {
type: STATUS_QUOTES_FETCH_SUCCESS,
statusId,
statuses: response.items,
next: response.next,
});
};
return dispatch(action);
}).catch(error => {
dispatch({
const action: FetchStatusQuotesFailAction = {
type: STATUS_QUOTES_FETCH_FAIL,
statusId,
error,
});
};
dispatch(action);
});
};
interface ExpandStatusQuotesRequestAction {
type: typeof STATUS_QUOTES_EXPAND_REQUEST;
statusId: string;
}
interface ExpandStatusQuotesSuccessAction {
type: typeof STATUS_QUOTES_EXPAND_SUCCESS;
statusId: string;
statuses: Array<BaseStatus>;
next: (() => Promise<PaginatedResponse<BaseStatus>>) | null;
}
interface ExpandStatusQuotesFailAction {
type: typeof STATUS_QUOTES_EXPAND_FAIL;
statusId: string;
error: unknown;
}
const expandStatusQuotes = (statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const next = getState().status_lists.get(`quotes:${statusId}`)?.next || null;
@ -50,28 +87,39 @@ const expandStatusQuotes = (statusId: string) =>
return dispatch(noOp);
}
dispatch({
const action: ExpandStatusQuotesRequestAction = {
type: STATUS_QUOTES_EXPAND_REQUEST,
statusId,
});
};
dispatch(action);
return next().then(response => {
dispatch(importFetchedStatuses(response.items));
dispatch({
const action: ExpandStatusQuotesSuccessAction = {
type: STATUS_QUOTES_EXPAND_SUCCESS,
statusId,
statuses: response.items,
next: response.next,
});
};
dispatch(action);
}).catch(error => {
dispatch({
const action: ExpandStatusQuotesFailAction = {
type: STATUS_QUOTES_EXPAND_FAIL,
statusId,
error,
});
};
dispatch(action);
});
};
type StatusQuotesAction =
| FetchStatusQuotesRequestAction
| FetchStatusQuotesSuccessAction
| FetchStatusQuotesFailAction
| ExpandStatusQuotesRequestAction
| ExpandStatusQuotesSuccessAction
| ExpandStatusQuotesFailAction;
export {
STATUS_QUOTES_FETCH_REQUEST,
STATUS_QUOTES_FETCH_SUCCESS,
@ -81,4 +129,5 @@ export {
STATUS_QUOTES_EXPAND_FAIL,
fetchStatusQuotes,
expandStatusQuotes,
type StatusQuotesAction,
};

View file

@ -332,6 +332,11 @@ const changeStatusLanguage = (statusId: string, language: string) => ({
language,
});
type StatusesAction =
| ReturnType<typeof undoStatusTranslation>
| ReturnType<typeof unfilterStatus>
| ReturnType<typeof changeStatusLanguage>;
export {
STATUS_CREATE_REQUEST,
STATUS_CREATE_SUCCESS,
@ -379,4 +384,5 @@ export {
undoStatusTranslation,
unfilterStatus,
changeStatusLanguage,
type StatusesAction,
};

View file

@ -105,14 +105,14 @@ interface TimelineDeleteAction {
statusId: string;
accountId: string;
references: ImmutableMap<string, readonly [statusId: string, accountId: string]>;
reblogOf: unknown;
reblogOf: string | null;
}
const deleteFromTimelines = (statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const accountId = getState().statuses.get(statusId)?.account?.id!;
const references = getState().statuses.filter(status => status.reblog_id === statusId).map(status => [status.id, status.account.id] as const);
const reblogOf = getState().statuses.getIn([statusId, 'reblog'], null);
const reblogOf = getState().statuses.get(statusId)?.reblog_id || null;
const action: TimelineDeleteAction = {
type: TIMELINE_DELETE,
@ -125,9 +125,7 @@ const deleteFromTimelines = (statusId: string) =>
dispatch(action);
};
const clearTimeline = (timeline: string) =>
(dispatch: AppDispatch) =>
dispatch({ type: TIMELINE_CLEAR, timeline });
const clearTimeline = (timeline: string) => ({ type: TIMELINE_CLEAR, timeline });
const noOp = () => { };

View file

@ -23,7 +23,7 @@ const StatusLanguagePicker: React.FC<IStatusLanguagePicker> = ({ status, showLab
const intl = useIntl();
const dispatch = useAppDispatch();
if (!status.contentMapHtml || !Object.keys(status.contentMapHtml).length) return null;
if (!status.contentMapHtml || Object.keys(status.contentMapHtml).length < 2) return null;
const icon = <Icon className='h-5 w-5 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/outline/language.svg')} />;

View file

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { translateStatus, undoStatusTranslation } from 'soapbox/actions/statuses';
@ -19,7 +19,6 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
const features = useFeatures();
const instance = useInstance();
const settings = useSettings();
const [autoTranslating, setAutoTranslating] = useState(false);
const autoTranslate = settings.autoTranslate;
const knownLanguages = autoTranslate ? [...settings.knownLanguages, intl.locale] : [intl.locale];
@ -46,16 +45,13 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
};
useEffect(() => {
if (!status.translation && settings.autoTranslate && features.translations && renderTranslate && supportsLanguages && status.translation !== false && status.language !== null && !knownLanguages.includes(status.language)) {
setAutoTranslating(true);
if (status.translation === null && settings.autoTranslate && features.translations && renderTranslate && supportsLanguages && status.translation !== false && status.language !== null && !knownLanguages.includes(status.language)) {
dispatch(translateStatus(status.id, intl.locale, true));
}
}, []);
if (!features.translations || !renderTranslate || !supportsLanguages || status.translation === false) return null;
if (settings.autoTranslate && !status.translating) return null;
const button = (
<button className='w-fit' onClick={handleTranslate}>
<HStack alignItems='center' space={1} className='text-primary-600 hover:underline dark:text-accent-blue'>
@ -77,8 +73,6 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
);
if (status.translation) {
if (autoTranslating) return null;
const languageNames = new Intl.DisplayNames([intl.locale], { type: 'language' });
const languageName = languageNames.of(status.language!);
const provider = status.translation.provider;

View file

@ -103,6 +103,7 @@ const ReactionsModal: React.FC<BaseModalProps & ReactionsModalProps> = ({ onClos
})}
listClassName='max-w-full'
itemClassName='pb-3'
style={{ height: '80vh' }}
>
{accounts.map((account) =>
<AccountContainer key={`${account.id}-${account.reaction}`} id={account.id} emoji={account.reaction} emojiUrl={account.reactionUrl} />,

View file

@ -1,14 +1,14 @@
import { useFloating } from '@floating-ui/react';
import clsx from 'clsx';
import throttle from 'lodash/throttle';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { fetchOwnAccounts, logOut, switchAccount } from 'soapbox/actions/auth';
import Account from 'soapbox/components/account';
import DropdownMenu from 'soapbox/components/dropdown-menu';
import { MenuDivider } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector, useClickOutside, useFeatures } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
import ThemeToggle from './theme-toggle';
@ -41,8 +41,6 @@ const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
const features = useFeatures();
const intl = useIntl();
const [visible, setVisible] = useState(false);
const { x, y, strategy, refs } = useFloating<HTMLButtonElement>({ placement: 'bottom-end' });
const authUsers = useAppSelector((state) => state.auth.users);
const otherAccounts = useAppSelector((state) => authUsers.map((authUser: any) => getAccount(state, authUser.id)!));
@ -62,7 +60,7 @@ const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
<Account account={account} showProfileHoverCard={false} withLinkToProfile={false} hideActions />
);
const menu: IMenuItem[] = useMemo(() => {
const ProfileDropdownMenu = useMemo(() => {
const menu: IMenuItem[] = [];
menu.push({ text: renderAccount(account), to: `/@${account.acct}` });
@ -93,47 +91,30 @@ const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
icon: require('@tabler/icons/outline/logout.svg'),
});
return menu;
return () => (
<>
{menu.map((menuItem, i) => (
<MenuItem key={i} menuItem={menuItem} />
))}
</>
);
}, [account, authUsers, features]);
const toggleVisible = () => setVisible(!visible);
useEffect(() => {
fetchOwnAccountThrottled();
}, [account, authUsers]);
useClickOutside(refs, () => {
setVisible(false);
});
return (
<>
<DropdownMenu
component={ProfileDropdownMenu}
>
<button
className='w-full rounded-full focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:ring-gray-800 dark:ring-offset-0 dark:focus:ring-primary-500'
type='button'
ref={refs.setReference}
onClick={toggleVisible}
>
{children}
</button>
{visible && (
<div
ref={refs.setFloating}
className='z-[1003] mt-2 max-w-xs rounded-md bg-white shadow-lg focus:outline-none black:bg-black dark:bg-gray-900 dark:ring-2 dark:ring-primary-700'
style={{
position: strategy,
top: y ?? 0,
left: x ?? 0,
width: 'max-content',
}}
>
{menu.map((menuItem, i) => (
<MenuItem key={i} menuItem={menuItem} />
))}
</div>
)}
</>
</DropdownMenu>
);
};

View file

@ -2,7 +2,7 @@ import { Map as ImmutableMap, Record as ImmutableRecord } from 'immutable';
import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, SET_ALERTS } from '../actions/push-notifications';
import type { AnyAction } from 'redux';
import type { SetterAction } from 'soapbox/actions/push-notifications/setter';
const SubscriptionRecord = ImmutableRecord({
id: '',
@ -25,7 +25,7 @@ const ReducerRecord = ImmutableRecord({
type Subscription = ReturnType<typeof SubscriptionRecord>;
const push_subscriptions = (state = ReducerRecord(), action: AnyAction) => {
const push_subscriptions = (state = ReducerRecord(), action: SetterAction) => {
switch (action.type) {
case SET_SUBSCRIPTION:
return state

View file

@ -162,7 +162,7 @@ const shouldDelete = (timelineId: string, excludeAccount?: string) => {
return true;
};
const deleteStatus = (state: State, statusId: string, accountId: string, references: ImmutableMap<string, [string, string]> | Array<[string, string]>, excludeAccount?: string) =>
const deleteStatus = (state: State, statusId: string, references: ImmutableMap<string, [string, string]> | Array<[string, string]>, excludeAccount?: string) =>
state.withMutations(state => {
state.keySeq().forEach(timelineId => {
if (shouldDelete(timelineId, excludeAccount)) {
@ -173,7 +173,7 @@ const deleteStatus = (state: State, statusId: string, accountId: string, referen
// Remove reblogs of deleted status
references.forEach(ref => {
deleteStatus(state, ref[0], ref[1], [], excludeAccount);
deleteStatus(state, ref[0], [], excludeAccount);
});
});
@ -208,7 +208,7 @@ const filterTimelines = (state: State, relationship: Relationship, statuses: Imm
statuses.forEach(status => {
if (status.account.id !== relationship.id) return;
const references = buildReferencesTo(statuses, status);
deleteStatus(state, status.id, status.account.id, references, relationship.id);
deleteStatus(state, status.id, references, relationship.id);
});
});
@ -329,7 +329,7 @@ const timelines = (state: State = initialState, action: AnyAction) => {
case TIMELINE_DEQUEUE:
return timelineDequeue(state, action.timeline);
case TIMELINE_DELETE:
return deleteStatus(state, action.statusId, action.accountId, action.references, action.reblogOf);
return deleteStatus(state, action.statusId, action.references, action.reblogOf);
case TIMELINE_CLEAR:
return clearTimeline(state, action.timeline);
case ACCOUNT_BLOCK_SUCCESS: