[MM-62987] Include description on aria-label for last reply time, make link focused when using up/down arrows. (#30837)

* [MM-62987] Include description on aria-label for last reply time, make link focused when using up/down arrows.

* PR feedback

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Devin Binnie
2025-05-08 10:50:14 -04:00
коммит произвёл GitHub
родитель c33dbe8d37
Коммит 0058edb806
5 изменённых файлов: 566 добавлений и 464 удалений

Просмотреть файл

@@ -1,371 +1,449 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/threading/global_threads/thread_item should report total number of replies 1`] = ` exports[`components/threading/global_threads/thread_item should report total number of replies 1`] = `
<div <Fragment>
aria-label="Thread by Someone"
className="ThreadItem"
id=""
onClick={[Function]}
onKeyDown={[Function]}
role="link"
tabIndex={0}
>
<header>
<div
className="ThreadItem__author"
>
Someone
</div>
<div
className="ThreadItem__tags"
>
<Memo(Tag)
onClick={[Function]}
text="Team name"
/>
</div>
<MockTimestamp
className="alt-hidden"
day="numeric"
units={
Array [
"now",
"minute",
"hour",
"day",
"week",
]
}
useTime={false}
/>
</header>
<div <div
className="menu-anchor alt-visible" aria-describedby="ThreadItem__timestamp_1y8hpek81byspd4enyk9mp1ncw"
> aria-label="Thread by Someone"
<MockThreadMenu className="ThreadItem"
hasUnreads={false} id=""
isFollowing={true}
threadId="1y8hpek81byspd4enyk9mp1ncw"
unreadTimestamp={1611786714912}
>
<WithTooltip
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Actions"
id="threading.threadItem.menu"
/>
}
>
<Memo(Button)
aria-label="Actions"
className="Button___icon"
marginTop={true}
>
<DotsVerticalIcon
size={18}
/>
</Memo(Button)>
</WithTooltip>
</MockThreadMenu>
</div>
<div
className="preview"
dir="auto"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
role="link"
tabIndex={0}
> >
<MockMarkdown <header>
imageProps={ <div
Object { className="ThreadItem__author"
"onImageHeightChanged": [Function], >
"onImageLoaded": [Function], Someone
</div>
<div
className="ThreadItem__tags"
>
<Memo(Tag)
onClick={[Function]}
text="Team name"
/>
</div>
<MockTimestamp
className="alt-hidden"
day="numeric"
units={
Array [
"now",
"minute",
"hour",
"day",
"week",
]
} }
} useTime={false}
mentionKeys={Array []} />
message="test msg" </header>
options={ <div
Object { className="menu-anchor alt-visible"
"atMentions": true, >
"mentionHighlight": false, <MockThreadMenu
"singleline": true, hasUnreads={false}
isFollowing={true}
threadId="1y8hpek81byspd4enyk9mp1ncw"
unreadTimestamp={1611786714912}
>
<WithTooltip
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Actions"
id="threading.threadItem.menu"
/>
}
>
<Memo(Button)
aria-label="Actions"
className="Button___icon"
marginTop={true}
>
<DotsVerticalIcon
size={18}
/>
</Memo(Button)>
</WithTooltip>
</MockThreadMenu>
</div>
<div
className="preview"
dir="auto"
onClick={[Function]}
onKeyDown={[Function]}
>
<MockMarkdown
imageProps={
Object {
"onImageHeightChanged": [Function],
"onImageLoaded": [Function],
}
} }
} mentionKeys={Array []}
/> message="test msg"
options={
Object {
"atMentions": true,
"mentionHighlight": false,
"singleline": true,
}
}
/>
</div>
<div
className="activity"
>
<MockAvatars
size="xs"
userIds={
Array [
"mt5td9mdriyapmwuh5pc84dmhr",
"ij61jet1bbdk8fhhxitywdj4ih",
"7n4ach3i53bbmj84dfmu5b7c1c",
]
}
/>
<MemoizedFormattedMessage
defaultMessage="{totalReplies, plural, =0 {Reply} =1 {# reply} other {# replies}}"
id="threading.numReplies"
values={
Object {
"totalReplies": 9,
}
}
/>
</div>
<span
className="sr-only"
id="ThreadItem__timestamp_1y8hpek81byspd4enyk9mp1ncw"
>
<MemoizedFormattedMessage
defaultMessage="Last reply "
id="threading.threadItem.timestamp"
/>
<MockTimestamp
className="alt-hidden"
day="numeric"
units={
Array [
"now",
"minute",
"hour",
"day",
"week",
]
}
useTime={false}
/>
</span>
</div> </div>
<div </Fragment>
className="activity"
>
<MockAvatars
size="xs"
userIds={
Array [
"mt5td9mdriyapmwuh5pc84dmhr",
"ij61jet1bbdk8fhhxitywdj4ih",
"7n4ach3i53bbmj84dfmu5b7c1c",
]
}
/>
<MemoizedFormattedMessage
defaultMessage="{totalReplies, plural, =0 {Reply} =1 {# reply} other {# replies}}"
id="threading.numReplies"
values={
Object {
"totalReplies": 9,
}
}
/>
</div>
</div>
`; `;
exports[`components/threading/global_threads/thread_item should report unread mentions 1`] = ` exports[`components/threading/global_threads/thread_item should report unread mentions 1`] = `
<div <Fragment>
aria-label="Thread by Someone"
className="ThreadItem has-unreads"
id=""
onClick={[Function]}
onKeyDown={[Function]}
role="link"
tabIndex={0}
>
<header>
<div
className="indicator"
>
<div
className="dot-mentions"
>
2
</div>
</div>
<div
className="ThreadItem__author"
>
Someone
</div>
<div
className="ThreadItem__tags"
>
<Memo(Tag)
onClick={[Function]}
text="Team name"
/>
</div>
<MockTimestamp
className="alt-hidden"
day="numeric"
units={
Array [
"now",
"minute",
"hour",
"day",
"week",
]
}
useTime={false}
/>
</header>
<div <div
className="menu-anchor alt-visible" aria-describedby="ThreadItem__timestamp_1y8hpek81byspd4enyk9mp1ncw"
> aria-label="Thread by Someone"
<MockThreadMenu className="ThreadItem has-unreads"
hasUnreads={true} id=""
isFollowing={true}
threadId="1y8hpek81byspd4enyk9mp1ncw"
unreadTimestamp={1611786714912}
>
<WithTooltip
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Actions"
id="threading.threadItem.menu"
/>
}
>
<Memo(Button)
aria-label="Actions"
className="Button___icon"
marginTop={true}
>
<DotsVerticalIcon
size={18}
/>
</Memo(Button)>
</WithTooltip>
</MockThreadMenu>
</div>
<div
className="preview"
dir="auto"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
role="link"
tabIndex={0}
> >
<MockMarkdown <header>
imageProps={ <div
Object { className="indicator"
"onImageHeightChanged": [Function], >
"onImageLoaded": [Function], <div
className="dot-mentions"
>
2
</div>
</div>
<div
className="ThreadItem__author"
>
Someone
</div>
<div
className="ThreadItem__tags"
>
<Memo(Tag)
onClick={[Function]}
text="Team name"
/>
</div>
<MockTimestamp
className="alt-hidden"
day="numeric"
units={
Array [
"now",
"minute",
"hour",
"day",
"week",
]
} }
} useTime={false}
mentionKeys={Array []} />
message="test msg" </header>
options={ <div
Object { className="menu-anchor alt-visible"
"atMentions": true, >
"mentionHighlight": false, <MockThreadMenu
"singleline": true, hasUnreads={true}
isFollowing={true}
threadId="1y8hpek81byspd4enyk9mp1ncw"
unreadTimestamp={1611786714912}
>
<WithTooltip
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Actions"
id="threading.threadItem.menu"
/>
}
>
<Memo(Button)
aria-label="Actions"
className="Button___icon"
marginTop={true}
>
<DotsVerticalIcon
size={18}
/>
</Memo(Button)>
</WithTooltip>
</MockThreadMenu>
</div>
<div
className="preview"
dir="auto"
onClick={[Function]}
onKeyDown={[Function]}
>
<MockMarkdown
imageProps={
Object {
"onImageHeightChanged": [Function],
"onImageLoaded": [Function],
}
} }
} mentionKeys={Array []}
/> message="test msg"
options={
Object {
"atMentions": true,
"mentionHighlight": false,
"singleline": true,
}
}
/>
</div>
<div
className="activity"
>
<MockAvatars
size="xs"
userIds={
Array [
"mt5td9mdriyapmwuh5pc84dmhr",
"ij61jet1bbdk8fhhxitywdj4ih",
"7n4ach3i53bbmj84dfmu5b7c1c",
]
}
/>
<MemoizedFormattedMessage
defaultMessage="{newReplies, plural, =1 {# new reply} other {# new replies}}"
id="threading.numNewReplies"
values={
Object {
"newReplies": 5,
}
}
/>
</div>
<span
className="sr-only"
id="ThreadItem__timestamp_1y8hpek81byspd4enyk9mp1ncw"
>
<MemoizedFormattedMessage
defaultMessage="Last reply "
id="threading.threadItem.timestamp"
/>
<MockTimestamp
className="alt-hidden"
day="numeric"
units={
Array [
"now",
"minute",
"hour",
"day",
"week",
]
}
useTime={false}
/>
</span>
</div> </div>
<div </Fragment>
className="activity"
>
<MockAvatars
size="xs"
userIds={
Array [
"mt5td9mdriyapmwuh5pc84dmhr",
"ij61jet1bbdk8fhhxitywdj4ih",
"7n4ach3i53bbmj84dfmu5b7c1c",
]
}
/>
<MemoizedFormattedMessage
defaultMessage="{newReplies, plural, =1 {# new reply} other {# new replies}}"
id="threading.numNewReplies"
values={
Object {
"newReplies": 5,
}
}
/>
</div>
</div>
`; `;
exports[`components/threading/global_threads/thread_item should report unread messages 1`] = ` exports[`components/threading/global_threads/thread_item should report unread messages 1`] = `
<div <Fragment>
aria-label="Thread by Someone"
className="ThreadItem has-unreads"
id=""
onClick={[Function]}
onKeyDown={[Function]}
role="link"
tabIndex={0}
>
<header>
<div
className="indicator"
>
<div
className="dot-unreads"
/>
</div>
<div
className="ThreadItem__author"
>
Someone
</div>
<div
className="ThreadItem__tags"
>
<Memo(Tag)
onClick={[Function]}
text="Team name"
/>
</div>
<MockTimestamp
className="alt-hidden"
day="numeric"
units={
Array [
"now",
"minute",
"hour",
"day",
"week",
]
}
useTime={false}
/>
</header>
<div <div
className="menu-anchor alt-visible" aria-describedby="ThreadItem__timestamp_1y8hpek81byspd4enyk9mp1ncw"
> aria-label="Thread by Someone"
<MockThreadMenu className="ThreadItem has-unreads"
hasUnreads={true} id=""
isFollowing={true}
threadId="1y8hpek81byspd4enyk9mp1ncw"
unreadTimestamp={1611786714912}
>
<WithTooltip
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Actions"
id="threading.threadItem.menu"
/>
}
>
<Memo(Button)
aria-label="Actions"
className="Button___icon"
marginTop={true}
>
<DotsVerticalIcon
size={18}
/>
</Memo(Button)>
</WithTooltip>
</MockThreadMenu>
</div>
<div
className="preview"
dir="auto"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
role="link"
tabIndex={0}
> >
<MockMarkdown <header>
imageProps={ <div
Object { className="indicator"
"onImageHeightChanged": [Function], >
"onImageLoaded": [Function], <div
className="dot-unreads"
/>
</div>
<div
className="ThreadItem__author"
>
Someone
</div>
<div
className="ThreadItem__tags"
>
<Memo(Tag)
onClick={[Function]}
text="Team name"
/>
</div>
<MockTimestamp
className="alt-hidden"
day="numeric"
units={
Array [
"now",
"minute",
"hour",
"day",
"week",
]
} }
} useTime={false}
mentionKeys={Array []} />
message="test msg" </header>
options={ <div
Object { className="menu-anchor alt-visible"
"atMentions": true, >
"mentionHighlight": false, <MockThreadMenu
"singleline": true, hasUnreads={true}
isFollowing={true}
threadId="1y8hpek81byspd4enyk9mp1ncw"
unreadTimestamp={1611786714912}
>
<WithTooltip
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Actions"
id="threading.threadItem.menu"
/>
}
>
<Memo(Button)
aria-label="Actions"
className="Button___icon"
marginTop={true}
>
<DotsVerticalIcon
size={18}
/>
</Memo(Button)>
</WithTooltip>
</MockThreadMenu>
</div>
<div
className="preview"
dir="auto"
onClick={[Function]}
onKeyDown={[Function]}
>
<MockMarkdown
imageProps={
Object {
"onImageHeightChanged": [Function],
"onImageLoaded": [Function],
}
} }
} mentionKeys={Array []}
/> message="test msg"
options={
Object {
"atMentions": true,
"mentionHighlight": false,
"singleline": true,
}
}
/>
</div>
<div
className="activity"
>
<MockAvatars
size="xs"
userIds={
Array [
"mt5td9mdriyapmwuh5pc84dmhr",
"ij61jet1bbdk8fhhxitywdj4ih",
"7n4ach3i53bbmj84dfmu5b7c1c",
]
}
/>
<MemoizedFormattedMessage
defaultMessage="{newReplies, plural, =1 {# new reply} other {# new replies}}"
id="threading.numNewReplies"
values={
Object {
"newReplies": 2,
}
}
/>
</div>
<span
className="sr-only"
id="ThreadItem__timestamp_1y8hpek81byspd4enyk9mp1ncw"
>
<MemoizedFormattedMessage
defaultMessage="Last reply "
id="threading.threadItem.timestamp"
/>
<MockTimestamp
className="alt-hidden"
day="numeric"
units={
Array [
"now",
"minute",
"hour",
"day",
"week",
]
}
useTime={false}
/>
</span>
</div> </div>
<div </Fragment>
className="activity"
>
<MockAvatars
size="xs"
userIds={
Array [
"mt5td9mdriyapmwuh5pc84dmhr",
"ij61jet1bbdk8fhhxitywdj4ih",
"7n4ach3i53bbmj84dfmu5b7c1c",
]
}
/>
<MemoizedFormattedMessage
defaultMessage="{newReplies, plural, =1 {# new reply} other {# new replies}}"
id="threading.numNewReplies"
values={
Object {
"newReplies": 2,
}
}
/>
</div>
</div>
`; `;

Просмотреть файл

@@ -266,7 +266,7 @@ describe('components/threading/global_threads/thread_item', () => {
test('should allow marking as unread on alt + click', () => { test('should allow marking as unread on alt + click', () => {
const wrapper = shallow(<ThreadItem {...props}/>); const wrapper = shallow(<ThreadItem {...props}/>);
wrapper.simulate('click', {altKey: true}); wrapper.find('div.ThreadItem').simulate('click', {altKey: true});
expect(updateThreadRead).not.toHaveBeenCalled(); expect(updateThreadRead).not.toHaveBeenCalled();
expect(markLastPostInThreadAsUnread).toHaveBeenCalledWith('user_id', 'tid', '1y8hpek81byspd4enyk9mp1ncw'); expect(markLastPostInThreadAsUnread).toHaveBeenCalledWith('user_id', 'tid', '1y8hpek81byspd4enyk9mp1ncw');
expect(manuallyMarkThreadAsUnread).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw', 1611786714912); expect(manuallyMarkThreadAsUnread).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw', 1611786714912);

Просмотреть файл

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import classNames from 'classnames'; import classNames from 'classnames';
import React, {memo, useCallback, useEffect, useMemo} from 'react'; import React, {memo, useCallback, useEffect, useMemo, useRef} from 'react';
import type {MouseEvent, KeyboardEvent} from 'react'; import type {MouseEvent, KeyboardEvent} from 'react';
import {FormattedMessage, useIntl} from 'react-intl'; import {FormattedMessage, useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux'; import {useDispatch, useSelector} from 'react-redux';
@@ -94,6 +94,7 @@ function ThreadItem({
const postAuthor = ensureString(post.props?.override_username) || displayName; const postAuthor = ensureString(post.props?.override_username) || displayName;
const getMentionKeysForPost = useMemo(() => makeGetMentionKeysForPost(), []); const getMentionKeysForPost = useMemo(() => makeGetMentionKeysForPost(), []);
const mentionsKeys = useSelector((state: GlobalState) => getMentionKeysForPost(state, post, channel)); const mentionsKeys = useSelector((state: GlobalState) => getMentionKeysForPost(state, post, channel));
const ref = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
if (channel?.teammate_id) { if (channel?.teammate_id) {
@@ -107,6 +108,12 @@ function ThreadItem({
} }
}, [channel, thread?.post.channel_id]); }, [channel, thread?.post.channel_id]);
useEffect(() => {
if (isSelected) {
ref.current?.focus();
}
}, [isSelected, threadId]);
const participantIds = useMemo(() => { const participantIds = useMemo(() => {
const ids = (thread?.participants || []).flatMap(({id}) => { const ids = (thread?.participants || []).flatMap(({id}) => {
if (id === post.user_id) { if (id === post.user_id) {
@@ -188,134 +195,152 @@ function ThreadItem({
} }
return ( return (
<div <>
style={style} <div
className={classNames('ThreadItem', { style={style}
'has-unreads': newReplies, className={classNames('ThreadItem', {
'is-selected': isSelected, 'has-unreads': newReplies,
})} 'is-selected': isSelected,
tabIndex={isSelected ? -1 : 0} })}
role='link' tabIndex={isSelected ? -1 : 0}
aria-label={formatMessage( role='link'
{id: 'threading.threadItem.ariaLabel', defaultMessage: 'Thread by {author}'}, aria-label={formatMessage(
{author: postAuthor}, {id: 'threading.threadItem.ariaLabel', defaultMessage: 'Thread by {author}'},
)} {author: postAuthor},
id={isFirstThreadInList ? 'tutorial-threads-mobile-list' : ''} )}
onClick={selectHandler} aria-describedby={`ThreadItem__timestamp_${threadId}`}
onKeyDown={selectHandler} id={isFirstThreadInList ? 'tutorial-threads-mobile-list' : ''}
> onClick={selectHandler}
<header> onKeyDown={selectHandler}
{Boolean(newMentions || newReplies) && ( ref={ref}
<div className='indicator'> >
{newMentions ? ( <header>
<div className={classNames('dot-mentions', {over: newMentions > 99})}> {Boolean(newMentions || newReplies) && (
{Math.min(newMentions, 99)} <div className='indicator'>
{newMentions > 99 && '+'} {newMentions ? (
</div> <div className={classNames('dot-mentions', {over: newMentions > 99})}>
) : ( {Math.min(newMentions, 99)}
<div className='dot-unreads'/> {newMentions > 99 && '+'}
</div>
) : (
<div className='dot-unreads'/>
)}
</div>
)}
<div className='ThreadItem__author'>{postAuthor}</div>
<div className='ThreadItem__tags'>
{channel && postAuthor !== channel?.display_name && (
<Tag
onClick={goToInChannelHandler}
text={channel?.display_name}
/>
)}
{isPostPriorityEnabled && (
thread.is_urgent && (
<PriorityBadge
className={postAuthor === channel?.display_name ? 'ml-2' : ''}
priority={PostPriority.URGENT}
/>
)
)} )}
</div> </div>
)} <Timestamp
<div className='ThreadItem__author'>{postAuthor}</div> {...THREADING_TIME}
<div className='ThreadItem__tags'> className='alt-hidden'
{channel && postAuthor !== channel?.display_name && ( value={lastReplyAt}
<Tag />
onClick={goToInChannelHandler} </header>
text={channel?.display_name} <div className='menu-anchor alt-visible'>
<ThreadMenu
threadId={threadId}
isFollowing={isFollowing ?? false}
hasUnreads={Boolean(newReplies)}
unreadTimestamp={unreadTimestamp}
>
<WithTooltip
title={(
<FormattedMessage
id='threading.threadItem.menu'
defaultMessage='Actions'
/>
)}
>
<Button
marginTop={true}
className='Button___icon'
aria-label={formatMessage({
id: 'threading.threadItem.menu',
defaultMessage: 'Actions',
})}
>
<DotsVerticalIcon size={18}/>
</Button>
</WithTooltip>
</ThreadMenu>
</div>
{/* The strange interaction here where we need a click/keydown handler messes with the ESLint rules, so we just disable it */}
{/*eslint-disable-next-line jsx-a11y/no-static-element-interactions*/}
<div
className='preview'
dir='auto'
onClick={handleFormattedTextClick}
onKeyDown={handleFormattedTextClick}
>
{post.message ? (
<Markdown
message={post.state === Posts.POST_DELETED ? msgDeleted : post.message}
options={markdownPreviewOptions}
imagesMetadata={post?.metadata && post?.metadata?.images}
mentionKeys={mentionsKeys}
imageProps={imageProps}
/> />
)} ) : (
{isPostPriorityEnabled && ( <Attachment post={post}/>
thread.is_urgent && (
<PriorityBadge
className={postAuthor === channel?.display_name ? 'ml-2' : ''}
priority={PostPriority.URGENT}
/>
)
)} )}
</div> </div>
<Timestamp <div className='activity'>
{...THREADING_TIME} {participantIds?.length ? (
className='alt-hidden' <Avatars
value={lastReplyAt} userIds={participantIds}
/> size='xs'
</header> />
<div className='menu-anchor alt-visible'> ) : null}
<ThreadMenu {Boolean(totalReplies) && (
threadId={threadId} <>
isFollowing={isFollowing ?? false} {newReplies ? (
hasUnreads={Boolean(newReplies)} <FormattedMessage
unreadTimestamp={unreadTimestamp} id='threading.numNewReplies'
defaultMessage='{newReplies, plural, =1 {# new reply} other {# new replies}}'
values={{newReplies}}
/>
) : (
<FormattedMessage
id='threading.numReplies'
defaultMessage='{totalReplies, plural, =0 {Reply} =1 {# reply} other {# replies}}'
values={{totalReplies}}
/>
)}
</>
)}
</div>
{showListTutorialTip && isFirstThreadInList && isMobileView && (<CRTListTutorialTip/>)}
<span
className='sr-only'
id={`ThreadItem__timestamp_${threadId}`}
> >
<WithTooltip <FormattedMessage
title={( id='threading.threadItem.timestamp'
<FormattedMessage defaultMessage='Last reply '
id='threading.threadItem.menu'
defaultMessage='Actions'
/>
)}
>
<Button
marginTop={true}
className='Button___icon'
aria-label={formatMessage({
id: 'threading.threadItem.menu',
defaultMessage: 'Actions',
})}
>
<DotsVerticalIcon size={18}/>
</Button>
</WithTooltip>
</ThreadMenu>
</div>
{/* The strange interaction here where we need a click/keydown handler messes with the ESLint rules, so we just disable it */}
{/*eslint-disable-next-line jsx-a11y/no-static-element-interactions*/}
<div
className='preview'
dir='auto'
onClick={handleFormattedTextClick}
onKeyDown={handleFormattedTextClick}
>
{post.message ? (
<Markdown
message={post.state === Posts.POST_DELETED ? msgDeleted : post.message}
options={markdownPreviewOptions}
imagesMetadata={post?.metadata && post?.metadata?.images}
mentionKeys={mentionsKeys}
imageProps={imageProps}
/> />
) : ( <Timestamp
<Attachment post={post}/> {...THREADING_TIME}
)} className='alt-hidden'
</div> value={lastReplyAt}
<div className='activity'>
{participantIds?.length ? (
<Avatars
userIds={participantIds}
size='xs'
/> />
) : null} </span>
{Boolean(totalReplies) && (
<>
{newReplies ? (
<FormattedMessage
id='threading.numNewReplies'
defaultMessage='{newReplies, plural, =1 {# new reply} other {# new replies}}'
values={{newReplies}}
/>
) : (
<FormattedMessage
id='threading.numReplies'
defaultMessage='{totalReplies, plural, =0 {Reply} =1 {# reply} other {# replies}}'
values={{totalReplies}}
/>
)}
</>
)}
</div> </div>
{showListTutorialTip && isFirstThreadInList && isMobileView && (<CRTListTutorialTip/>)} </>
</div>
); );
} }

Просмотреть файл

@@ -120,9 +120,7 @@ const ThreadList = ({
} }
} }
select(data[threadIdToSelect]); select(data[threadIdToSelect]);
e.preventDefault();
// hacky way to ensure the thread item loses focus.
ref.current?.focus();
}, [selectedThreadId, data]); }, [selectedThreadId, data]);
useEffect(() => { useEffect(() => {

Просмотреть файл

@@ -5497,6 +5497,7 @@
"threading.threadHeader.menu": "More Actions", "threading.threadHeader.menu": "More Actions",
"threading.threadItem.ariaLabel": "Thread by {author}", "threading.threadItem.ariaLabel": "Thread by {author}",
"threading.threadItem.menu": "Actions", "threading.threadItem.menu": "Actions",
"threading.threadItem.timestamp": "Last reply ",
"threading.threadList.markRead": "Mark all threads as read", "threading.threadList.markRead": "Mark all threads as read",
"threading.threadList.tabsLabel": "Filter visible threads", "threading.threadList.tabsLabel": "Filter visible threads",
"threading.threadMenu.copy": "Copy link", "threading.threadMenu.copy": "Copy link",