Fix keyboard shortcuts and navigation in grouped notifications (#31076)
parent
55705d8191
commit
af06d74574
|
@ -1,3 +1,5 @@
|
||||||
|
import { browserHistory } from 'mastodon/components/router';
|
||||||
|
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -676,3 +678,13 @@ export const updateAccount = ({ displayName, note, avatar, header, discoverable,
|
||||||
dispatch(importFetchedAccount(response.data));
|
dispatch(importFetchedAccount(response.data));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const navigateToProfile = (accountId) => {
|
||||||
|
return (_dispatch, getState) => {
|
||||||
|
const acct = getState().accounts.getIn([accountId, 'acct']);
|
||||||
|
|
||||||
|
if (acct) {
|
||||||
|
browserHistory.push(`/@${acct}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -122,6 +122,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,
|
||||||
|
@ -154,6 +166,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({
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { browserHistory } from 'mastodon/components/router';
|
||||||
|
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
|
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
|
||||||
|
@ -363,3 +365,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}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -119,6 +119,7 @@ class Status extends ImmutablePureComponent {
|
||||||
skipPrepend: PropTypes.bool,
|
skipPrepend: PropTypes.bool,
|
||||||
avatarSize: PropTypes.number,
|
avatarSize: PropTypes.number,
|
||||||
deployPictureInPicture: PropTypes.func,
|
deployPictureInPicture: PropTypes.func,
|
||||||
|
unfocusable: PropTypes.bool,
|
||||||
pictureInPicture: ImmutablePropTypes.contains({
|
pictureInPicture: ImmutablePropTypes.contains({
|
||||||
inUse: PropTypes.bool,
|
inUse: PropTypes.bool,
|
||||||
available: PropTypes.bool,
|
available: PropTypes.bool,
|
||||||
|
@ -355,7 +356,7 @@ class Status extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, hidden, featured, unread, showThread, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46 } = this.props;
|
const { intl, hidden, featured, unfocusable, unread, showThread, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46 } = this.props;
|
||||||
|
|
||||||
let { status, account, ...other } = this.props;
|
let { status, account, ...other } = this.props;
|
||||||
|
|
||||||
|
@ -381,8 +382,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={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex={0}>
|
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} 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>
|
||||||
|
@ -402,8 +403,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}>
|
||||||
|
@ -550,8 +551,8 @@ class Status extends ImmutablePureComponent {
|
||||||
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
|
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={handlers}>
|
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
|
||||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
|
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted || unfocusable ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
|
||||||
{!skipPrepend && prepend}
|
{!skipPrepend && prepend}
|
||||||
|
|
||||||
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted })} data-id={status.get('id')}>
|
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted })} data-id={status.get('id')}>
|
||||||
|
|
|
@ -2,8 +2,10 @@ import { useMemo } from 'react';
|
||||||
|
|
||||||
import { HotKeys } from 'react-hotkeys';
|
import { HotKeys } from 'react-hotkeys';
|
||||||
|
|
||||||
|
import { navigateToProfile } from 'mastodon/actions/accounts';
|
||||||
|
import { mentionComposeById } from 'mastodon/actions/compose';
|
||||||
import type { NotificationGroup as NotificationGroupModel } from 'mastodon/models/notification_group';
|
import type { NotificationGroup as NotificationGroupModel } from 'mastodon/models/notification_group';
|
||||||
import { useAppSelector } from 'mastodon/store';
|
import { useAppSelector, useAppDispatch } from 'mastodon/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;
|
||||||
|
|
|
@ -2,9 +2,14 @@ import { useMemo } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { HotKeys } from 'react-hotkeys';
|
||||||
|
|
||||||
|
import { replyComposeById } from 'mastodon/actions/compose';
|
||||||
|
import { navigateToStatus } from 'mastodon/actions/statuses';
|
||||||
import type { IconProp } from 'mastodon/components/icon';
|
import type { IconProp } from 'mastodon/components/icon';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||||
|
import { useAppDispatch } from 'mastodon/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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,10 +2,18 @@ import { useMemo } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { HotKeys } from 'react-hotkeys';
|
||||||
|
|
||||||
|
import { replyComposeById } from 'mastodon/actions/compose';
|
||||||
|
import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions';
|
||||||
|
import {
|
||||||
|
navigateToStatus,
|
||||||
|
toggleStatusSpoilers,
|
||||||
|
} from 'mastodon/actions/statuses';
|
||||||
import type { IconProp } from 'mastodon/components/icon';
|
import type { IconProp } from 'mastodon/components/icon';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import Status from 'mastodon/containers/status_container';
|
import Status from 'mastodon/containers/status_container';
|
||||||
import { useAppSelector } from 'mastodon/store';
|
import { useAppSelector, useAppDispatch } from 'mastodon/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 +37,8 @@ export const NotificationWithStatus: React.FC<{
|
||||||
type,
|
type,
|
||||||
unread,
|
unread,
|
||||||
}) => {
|
}) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const label = useMemo(
|
const label = useMemo(
|
||||||
() =>
|
() =>
|
||||||
labelRenderer({
|
labelRenderer({
|
||||||
|
@ -41,33 +51,61 @@ 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: () => {
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue