[Glitch] Fix keyboard shortcuts and navigation in grouped notifications

Port af06d74574 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
pull/2791/head
Claire 2024-07-23 08:20:17 +02:00
parent 68000956f4
commit 838ea5f9cd
7 changed files with 191 additions and 66 deletions

View File

@ -1,3 +1,5 @@
import { browserHistory } from 'flavours/glitch/components/router';
import api, { getLinks } from '../api'; import api, { getLinks } from '../api';
import { import {
@ -722,6 +724,16 @@ export const updateAccount = ({ displayName, note, avatar, header, discoverable,
}); });
}; };
export const navigateToProfile = (accountId) => {
return (_dispatch, getState) => {
const acct = getState().accounts.getIn([accountId, 'acct']);
if (acct) {
browserHistory.push(`/@${acct}`);
}
};
};
export function fetchPinnedAccountsSuggestions(q) { export function fetchPinnedAccountsSuggestions(q) {
return (dispatch) => { return (dispatch) => {
dispatch(fetchPinnedAccountsSuggestionsRequest()); dispatch(fetchPinnedAccountsSuggestionsRequest());

View File

@ -131,6 +131,18 @@ export function replyCompose(status) {
}; };
} }
export function replyComposeById(statusId) {
return (dispatch, getState) => {
const state = getState();
const status = state.statuses.get(statusId);
if (status) {
const account = state.accounts.get(status.get('account'));
dispatch(replyCompose(status.set('account', account)));
}
};
}
export function cancelReplyCompose() { export function cancelReplyCompose() {
return { return {
type: COMPOSE_REPLY_CANCEL, type: COMPOSE_REPLY_CANCEL,
@ -163,6 +175,12 @@ export function mentionCompose(account) {
}; };
} }
export function mentionComposeById(accountId) {
return (dispatch, getState) => {
dispatch(mentionCompose(getState().accounts.get(accountId)));
};
}
export function directCompose(account) { export function directCompose(account) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ dispatch({

View File

@ -1,3 +1,5 @@
import { browserHistory } from 'flavours/glitch/components/router';
import api from '../api'; import api from '../api';
import { ensureComposeIsVisible, setComposeToStatus } from './compose'; import { ensureComposeIsVisible, setComposeToStatus } from './compose';
@ -364,3 +366,15 @@ export const undoStatusTranslation = (id, pollId) => ({
id, id,
pollId, pollId,
}); });
export const navigateToStatus = (statusId) => {
return (_dispatch, getState) => {
const state = getState();
const accountId = state.statuses.getIn([statusId, 'account']);
const acct = state.accounts.getIn([accountId, 'acct']);
if (acct) {
browserHistory.push(`/@${acct}/${statusId}`);
}
};
};

View File

@ -520,7 +520,7 @@ class Status extends ImmutablePureComponent {
} }
render () { render () {
const { intl, hidden, featured, unread, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46 } = this.props; const { intl, hidden, featured, unfocusable, unread, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46 } = this.props;
const { const {
parseClick, parseClick,
@ -585,8 +585,8 @@ class Status extends ImmutablePureComponent {
if (hidden) { if (hidden) {
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
<div ref={this.handleRef} className='status focusable' tabIndex={0}> <div ref={this.handleRef} className='status focusable' tabIndex={unfocusable ? null : 0}>
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span> <span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
<span>{status.get('content')}</span> <span>{status.get('content')}</span>
</div> </div>
@ -606,8 +606,8 @@ class Status extends ImmutablePureComponent {
}; };
return ( return (
<HotKeys handlers={minHandlers}> <HotKeys handlers={minHandlers} tabIndex={unfocusable ? null : -1}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex={0} ref={this.handleRef}> <div className='status__wrapper status__wrapper--filtered focusable' tabIndex={unfocusable ? null : 0} ref={this.handleRef}>
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(', ')}. <FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(', ')}.
{' '} {' '}
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}> <button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
@ -786,11 +786,11 @@ class Status extends ImmutablePureComponent {
contentMedia.push(hashtagBar); contentMedia.push(hashtagBar);
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
<div <div
className={classNames('status__wrapper', 'focusable', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, collapsed: isCollapsed })} className={classNames('status__wrapper', 'focusable', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, collapsed: isCollapsed })}
{...selectorAttribs} {...selectorAttribs}
tabIndex={0} tabIndex={unfocusable ? null : 0}
data-featured={featured ? 'true' : null} data-featured={featured ? 'true' : null}
aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}
ref={this.handleRef} ref={this.handleRef}

View File

@ -2,8 +2,10 @@ import { useMemo } from 'react';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import { navigateToProfile } from 'flavours/glitch/actions/accounts';
import { mentionComposeById } from 'flavours/glitch/actions/compose';
import type { NotificationGroup as NotificationGroupModel } from 'flavours/glitch/models/notification_group'; import type { NotificationGroup as NotificationGroupModel } from 'flavours/glitch/models/notification_group';
import { useAppSelector } from 'flavours/glitch/store'; import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
import { NotificationAdminReport } from './notification_admin_report'; import { NotificationAdminReport } from './notification_admin_report';
import { NotificationAdminSignUp } from './notification_admin_sign_up'; import { NotificationAdminSignUp } from './notification_admin_sign_up';
@ -30,6 +32,13 @@ export const NotificationGroup: React.FC<{
), ),
); );
const dispatch = useAppDispatch();
const accountId =
notificationGroup?.type === 'gap'
? undefined
: notificationGroup?.sampleAccountIds[0];
const handlers = useMemo( const handlers = useMemo(
() => ({ () => ({
moveUp: () => { moveUp: () => {
@ -39,8 +48,16 @@ export const NotificationGroup: React.FC<{
moveDown: () => { moveDown: () => {
onMoveDown(notificationGroupId); onMoveDown(notificationGroupId);
}, },
openProfile: () => {
if (accountId) dispatch(navigateToProfile(accountId));
},
mention: () => {
if (accountId) dispatch(mentionComposeById(accountId));
},
}), }),
[notificationGroupId, onMoveUp, onMoveDown], [dispatch, notificationGroupId, accountId, onMoveUp, onMoveDown],
); );
if (!notificationGroup || notificationGroup.type === 'gap') return null; if (!notificationGroup || notificationGroup.type === 'gap') return null;

View File

@ -2,9 +2,14 @@ import { useMemo } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { HotKeys } from 'react-hotkeys';
import { replyComposeById } from 'flavours/glitch/actions/compose';
import { navigateToStatus } from 'flavours/glitch/actions/statuses';
import type { IconProp } from 'flavours/glitch/components/icon'; import type { IconProp } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp'; import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
import { useAppDispatch } from 'flavours/glitch/store';
import { AvatarGroup } from './avatar_group'; import { AvatarGroup } from './avatar_group';
import { EmbeddedStatus } from './embedded_status'; import { EmbeddedStatus } from './embedded_status';
@ -39,6 +44,8 @@ export const NotificationGroupWithStatus: React.FC<{
type, type,
unread, unread,
}) => { }) => {
const dispatch = useAppDispatch();
const label = useMemo( const label = useMemo(
() => () =>
labelRenderer({ labelRenderer({
@ -53,39 +60,54 @@ export const NotificationGroupWithStatus: React.FC<{
[labelRenderer, accountIds, count, labelSeeMoreHref], [labelRenderer, accountIds, count, labelSeeMoreHref],
); );
const handlers = useMemo(
() => ({
open: () => {
dispatch(navigateToStatus(statusId));
},
reply: () => {
dispatch(replyComposeById(statusId));
},
}),
[dispatch, statusId],
);
return ( return (
<div <HotKeys handlers={handlers}>
role='button' <div
className={classNames( role='button'
`notification-group focusable notification-group--${type}`, className={classNames(
{ 'notification-group--unread': unread }, `notification-group focusable notification-group--${type}`,
)} { 'notification-group--unread': unread },
tabIndex={0} )}
> tabIndex={0}
<div className='notification-group__icon'> >
<Icon icon={icon} id={iconId} /> <div className='notification-group__icon'>
</div> <Icon icon={icon} id={iconId} />
<div className='notification-group__main'>
<div className='notification-group__main__header'>
<div className='notification-group__main__header__wrapper'>
<AvatarGroup accountIds={accountIds} />
{actions}
</div>
<div className='notification-group__main__header__label'>
{label}
{timestamp && <RelativeTimestamp timestamp={timestamp} />}
</div>
</div> </div>
{statusId && ( <div className='notification-group__main'>
<div className='notification-group__main__status'> <div className='notification-group__main__header'>
<EmbeddedStatus statusId={statusId} /> <div className='notification-group__main__header__wrapper'>
<AvatarGroup accountIds={accountIds} />
{actions}
</div>
<div className='notification-group__main__header__label'>
{label}
{timestamp && <RelativeTimestamp timestamp={timestamp} />}
</div>
</div> </div>
)}
{statusId && (
<div className='notification-group__main__status'>
<EmbeddedStatus statusId={statusId} />
</div>
)}
</div>
</div> </div>
</div> </HotKeys>
); );
}; };

View File

@ -2,10 +2,21 @@ import { useMemo } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { HotKeys } from 'react-hotkeys';
import { replyComposeById } from 'flavours/glitch/actions/compose';
import {
toggleReblog,
toggleFavourite,
} from 'flavours/glitch/actions/interactions';
import {
navigateToStatus,
toggleStatusSpoilers,
} from 'flavours/glitch/actions/statuses';
import type { IconProp } from 'flavours/glitch/components/icon'; import type { IconProp } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import Status from 'flavours/glitch/containers/status_container'; import Status from 'flavours/glitch/containers/status_container';
import { useAppSelector } from 'flavours/glitch/store'; import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
import { NamesList } from './names_list'; import { NamesList } from './names_list';
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
@ -29,6 +40,8 @@ export const NotificationWithStatus: React.FC<{
type, type,
unread, unread,
}) => { }) => {
const dispatch = useAppDispatch();
const label = useMemo( const label = useMemo(
() => () =>
labelRenderer({ labelRenderer({
@ -41,33 +54,62 @@ export const NotificationWithStatus: React.FC<{
(state) => state.statuses.getIn([statusId, 'visibility']) === 'direct', (state) => state.statuses.getIn([statusId, 'visibility']) === 'direct',
); );
return ( const handlers = useMemo(
<div () => ({
role='button' open: () => {
className={classNames( dispatch(navigateToStatus(statusId));
`notification-ungrouped focusable notification-ungrouped--${type}`, },
{
'notification-ungrouped--unread': unread,
'notification-ungrouped--direct': isPrivateMention,
},
)}
tabIndex={0}
>
<div className='notification-ungrouped__header'>
<div className='notification-ungrouped__header__icon'>
<Icon icon={icon} id={iconId} />
</div>
{label}
</div>
<Status reply: () => {
// @ts-expect-error -- <Status> is not yet typed dispatch(replyComposeById(statusId));
id={statusId} },
contextType='notifications'
withDismiss boost: () => {
skipPrepend dispatch(toggleReblog(statusId));
avatarSize={40} },
/>
</div> favourite: () => {
dispatch(toggleFavourite(statusId));
},
toggleHidden: () => {
// TODO: glitch-soc is different and needs different handling of CWs
dispatch(toggleStatusSpoilers(statusId));
},
}),
[dispatch, statusId],
);
return (
<HotKeys handlers={handlers}>
<div
role='button'
className={classNames(
`notification-ungrouped focusable notification-ungrouped--${type}`,
{
'notification-ungrouped--unread': unread,
'notification-ungrouped--direct': isPrivateMention,
},
)}
tabIndex={0}
>
<div className='notification-ungrouped__header'>
<div className='notification-ungrouped__header__icon'>
<Icon icon={icon} id={iconId} />
</div>
{label}
</div>
<Status
// @ts-expect-error -- <Status> is not yet typed
id={statusId}
contextType='notifications'
withDismiss
skipPrepend
avatarSize={40}
unfocusable
/>
</div>
</HotKeys>
); );
}; };