Adds keyboard affordance to story viewer
This commit is contained in:
parent
09fbfe5421
commit
19bb3bc994
5 changed files with 224 additions and 196 deletions
|
@ -10,6 +10,12 @@
|
||||||
height: 96px;
|
height: 96px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
@include keyboard-mode {
|
||||||
|
&:focus {
|
||||||
|
background: $color-gray-65;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: $color-gray-65;
|
background: $color-gray-65;
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,12 @@
|
||||||
width: 24px;
|
width: 24px;
|
||||||
|
|
||||||
@include color-svg('../images/icons/v2/more-horiz-24.svg', $color-white);
|
@include color-svg('../images/icons/v2/more-horiz-24.svg', $color-white);
|
||||||
|
|
||||||
|
@include keyboard-mode {
|
||||||
|
&:focus {
|
||||||
|
background-color: $color-ultramarine;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__container {
|
&__container {
|
||||||
|
@ -89,6 +95,11 @@
|
||||||
|
|
||||||
&__reply {
|
&__reply {
|
||||||
@include button-reset;
|
@include button-reset;
|
||||||
|
@include keyboard-mode {
|
||||||
|
&:focus {
|
||||||
|
color: $color-ultramarine;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__progress {
|
&__progress {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
@ -82,51 +83,54 @@ export const StoriesPane = ({
|
||||||
}, [searchTerm, stories]);
|
}, [searchTerm, stories]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<FocusTrap>
|
||||||
<div className="Stories__pane__header">
|
<div>
|
||||||
<button
|
<div className="Stories__pane__header">
|
||||||
aria-label={i18n('back')}
|
<button
|
||||||
className="Stories__pane__header--back"
|
aria-label={i18n('back')}
|
||||||
onClick={onBack}
|
className="Stories__pane__header--back"
|
||||||
type="button"
|
onClick={onBack}
|
||||||
|
tabIndex={0}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
<div className="Stories__pane__header--title">
|
||||||
|
{i18n('Stories__title')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SearchInput
|
||||||
|
i18n={i18n}
|
||||||
|
moduleClassName="Stories__search"
|
||||||
|
onChange={event => {
|
||||||
|
setSearchTerm(event.target.value);
|
||||||
|
}}
|
||||||
|
placeholder={i18n('search')}
|
||||||
|
value={searchTerm}
|
||||||
/>
|
/>
|
||||||
<div className="Stories__pane__header--title">
|
<div
|
||||||
{i18n('Stories__title')}
|
className={classNames('Stories__pane__list', {
|
||||||
|
'Stories__pane__list--empty': !stories.length,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{renderedStories.map(story => (
|
||||||
|
<StoryListItem
|
||||||
|
key={getNewestStory(story).timestamp}
|
||||||
|
i18n={i18n}
|
||||||
|
onClick={() => {
|
||||||
|
onStoryClicked(story.conversationId);
|
||||||
|
}}
|
||||||
|
onHideStory={() => {
|
||||||
|
toggleHideStories(getNewestStory(story).sender.id);
|
||||||
|
}}
|
||||||
|
onGoToConversation={conversationId => {
|
||||||
|
openConversationInternal({ conversationId });
|
||||||
|
}}
|
||||||
|
queueStoryDownload={queueStoryDownload}
|
||||||
|
story={getNewestStory(story)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{!stories.length && i18n('Stories__list-empty')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SearchInput
|
</FocusTrap>
|
||||||
i18n={i18n}
|
|
||||||
moduleClassName="Stories__search"
|
|
||||||
onChange={event => {
|
|
||||||
setSearchTerm(event.target.value);
|
|
||||||
}}
|
|
||||||
placeholder={i18n('search')}
|
|
||||||
value={searchTerm}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={classNames('Stories__pane__list', {
|
|
||||||
'Stories__pane__list--empty': !stories.length,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{renderedStories.map(story => (
|
|
||||||
<StoryListItem
|
|
||||||
key={getNewestStory(story).timestamp}
|
|
||||||
i18n={i18n}
|
|
||||||
onClick={() => {
|
|
||||||
onStoryClicked(story.conversationId);
|
|
||||||
}}
|
|
||||||
onHideStory={() => {
|
|
||||||
toggleHideStories(getNewestStory(story).sender.id);
|
|
||||||
}}
|
|
||||||
onGoToConversation={conversationId => {
|
|
||||||
openConversationInternal({ conversationId });
|
|
||||||
}}
|
|
||||||
queueStoryDownload={queueStoryDownload}
|
|
||||||
story={getNewestStory(story)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{!stories.length && i18n('Stories__list-empty')}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -124,6 +124,7 @@ export const StoryListItem = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
|
tabIndex={0}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useSpring, animated, to } from '@react-spring/web';
|
import { useSpring, animated, to } from '@react-spring/web';
|
||||||
import type { BodyRangeType, LocalizerType } from '../types/Util';
|
import type { BodyRangeType, LocalizerType } from '../types/Util';
|
||||||
|
@ -197,170 +198,175 @@ export const StoryViewer = ({
|
||||||
}, [navigateStories]);
|
}, [navigateStories]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="StoryViewer">
|
<FocusTrap>
|
||||||
<div className="StoryViewer__overlay" />
|
<div className="StoryViewer">
|
||||||
<div className="StoryViewer__content">
|
<div className="StoryViewer__overlay" />
|
||||||
<button
|
<div className="StoryViewer__content">
|
||||||
aria-label={i18n('MyStories__more')}
|
<button
|
||||||
className="StoryViewer__more"
|
aria-label={i18n('MyStories__more')}
|
||||||
type="button"
|
className="StoryViewer__more"
|
||||||
/>
|
tabIndex={0}
|
||||||
<button
|
type="button"
|
||||||
aria-label={i18n('close')}
|
|
||||||
className="StoryViewer__close-button"
|
|
||||||
onClick={onClose}
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
<div className="StoryViewer__container">
|
|
||||||
<StoryImage
|
|
||||||
attachment={attachment}
|
|
||||||
i18n={i18n}
|
|
||||||
label={i18n('lightboxImageAlt')}
|
|
||||||
moduleClassName="StoryViewer__story"
|
|
||||||
queueStoryDownload={queueStoryDownload}
|
|
||||||
storyId={messageId}
|
|
||||||
/>
|
/>
|
||||||
<div className="StoryViewer__meta">
|
<button
|
||||||
<Avatar
|
aria-label={i18n('close')}
|
||||||
acceptedMessageRequest={acceptedMessageRequest}
|
className="StoryViewer__close-button"
|
||||||
avatarPath={avatarPath}
|
onClick={onClose}
|
||||||
badge={undefined}
|
tabIndex={0}
|
||||||
color={getAvatarColor(color)}
|
type="button"
|
||||||
conversationType="direct"
|
/>
|
||||||
|
<div className="StoryViewer__container">
|
||||||
|
<StoryImage
|
||||||
|
attachment={attachment}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isMe={Boolean(isMe)}
|
label={i18n('lightboxImageAlt')}
|
||||||
name={name}
|
moduleClassName="StoryViewer__story"
|
||||||
profileName={profileName}
|
queueStoryDownload={queueStoryDownload}
|
||||||
sharedGroupNames={sharedGroupNames}
|
storyId={messageId}
|
||||||
size={AvatarSize.TWENTY_EIGHT}
|
|
||||||
title={title}
|
|
||||||
/>
|
/>
|
||||||
{group && (
|
<div className="StoryViewer__meta">
|
||||||
<Avatar
|
<Avatar
|
||||||
acceptedMessageRequest={group.acceptedMessageRequest}
|
acceptedMessageRequest={acceptedMessageRequest}
|
||||||
avatarPath={group.avatarPath}
|
avatarPath={avatarPath}
|
||||||
badge={undefined}
|
badge={undefined}
|
||||||
className="StoryViewer__meta--group-avatar"
|
color={getAvatarColor(color)}
|
||||||
color={getAvatarColor(group.color)}
|
conversationType="direct"
|
||||||
conversationType="group"
|
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isMe={false}
|
isMe={Boolean(isMe)}
|
||||||
name={group.name}
|
name={name}
|
||||||
profileName={group.profileName}
|
profileName={profileName}
|
||||||
sharedGroupNames={group.sharedGroupNames}
|
sharedGroupNames={sharedGroupNames}
|
||||||
size={AvatarSize.TWENTY_EIGHT}
|
size={AvatarSize.TWENTY_EIGHT}
|
||||||
title={group.title}
|
title={title}
|
||||||
/>
|
/>
|
||||||
)}
|
{group && (
|
||||||
<div className="StoryViewer__meta--title">
|
<Avatar
|
||||||
{group
|
acceptedMessageRequest={group.acceptedMessageRequest}
|
||||||
? i18n('Stories__from-to-group', {
|
avatarPath={group.avatarPath}
|
||||||
name: title,
|
badge={undefined}
|
||||||
group: group.title,
|
className="StoryViewer__meta--group-avatar"
|
||||||
})
|
color={getAvatarColor(group.color)}
|
||||||
: title}
|
conversationType="group"
|
||||||
</div>
|
i18n={i18n}
|
||||||
<MessageTimestamp
|
isMe={false}
|
||||||
i18n={i18n}
|
name={group.name}
|
||||||
module="StoryViewer__meta--timestamp"
|
profileName={group.profileName}
|
||||||
timestamp={timestamp}
|
sharedGroupNames={group.sharedGroupNames}
|
||||||
/>
|
size={AvatarSize.TWENTY_EIGHT}
|
||||||
<div className="StoryViewer__progress">
|
title={group.title}
|
||||||
{stories.map((story, index) => (
|
/>
|
||||||
<div
|
)}
|
||||||
className="StoryViewer__progress--container"
|
<div className="StoryViewer__meta--title">
|
||||||
key={story.messageId}
|
{group
|
||||||
>
|
? i18n('Stories__from-to-group', {
|
||||||
{currentStoryIndex === index ? (
|
name: title,
|
||||||
<animated.div
|
group: group.title,
|
||||||
className="StoryViewer__progress--bar"
|
})
|
||||||
style={{
|
: title}
|
||||||
width: to([styles.width], width => `${width}%`),
|
</div>
|
||||||
}}
|
<MessageTimestamp
|
||||||
/>
|
i18n={i18n}
|
||||||
) : (
|
module="StoryViewer__meta--timestamp"
|
||||||
<div
|
timestamp={timestamp}
|
||||||
className="StoryViewer__progress--bar"
|
/>
|
||||||
style={{
|
<div className="StoryViewer__progress">
|
||||||
width: currentStoryIndex < index ? '0%' : '100%',
|
{stories.map((story, index) => (
|
||||||
}}
|
<div
|
||||||
/>
|
className="StoryViewer__progress--container"
|
||||||
)}
|
key={story.messageId}
|
||||||
</div>
|
>
|
||||||
))}
|
{currentStoryIndex === index ? (
|
||||||
|
<animated.div
|
||||||
|
className="StoryViewer__progress--bar"
|
||||||
|
style={{
|
||||||
|
width: to([styles.width], width => `${width}%`),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="StoryViewer__progress--bar"
|
||||||
|
style={{
|
||||||
|
width: currentStoryIndex < index ? '0%' : '100%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="StoryViewer__actions">
|
||||||
|
{isMe ? (
|
||||||
|
<>
|
||||||
|
{views &&
|
||||||
|
(views === 1 ? (
|
||||||
|
<Intl
|
||||||
|
i18n={i18n}
|
||||||
|
id="MyStories__views--singular"
|
||||||
|
components={[<strong>{views}</strong>]}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Intl
|
||||||
|
i18n={i18n}
|
||||||
|
id="MyStories__views--plural"
|
||||||
|
components={[<strong>{views}</strong>]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{views && replies && ' '}
|
||||||
|
{replies &&
|
||||||
|
(replies === 1 ? (
|
||||||
|
<Intl
|
||||||
|
i18n={i18n}
|
||||||
|
id="MyStories__replies--singular"
|
||||||
|
components={[<strong>{replies}</strong>]}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Intl
|
||||||
|
i18n={i18n}
|
||||||
|
id="MyStories__replies--plural"
|
||||||
|
components={[<strong>{replies}</strong>]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="StoryViewer__reply"
|
||||||
|
onClick={() => setHasReplyModal(true)}
|
||||||
|
tabIndex={0}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{i18n('StoryViewer__reply')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="StoryViewer__actions">
|
{hasReplyModal && (
|
||||||
{isMe ? (
|
<StoryViewsNRepliesModal
|
||||||
<>
|
authorTitle={title}
|
||||||
{views &&
|
getPreferredBadge={getPreferredBadge}
|
||||||
(views === 1 ? (
|
i18n={i18n}
|
||||||
<Intl
|
isMyStory={isMe}
|
||||||
i18n={i18n}
|
onClose={() => setHasReplyModal(false)}
|
||||||
id="MyStories__views--singular"
|
onReact={emoji => {
|
||||||
components={[<strong>{views}</strong>]}
|
onReactToStory(emoji, visibleStory);
|
||||||
/>
|
}}
|
||||||
) : (
|
onReply={(message, mentions, replyTimestamp) => {
|
||||||
<Intl
|
setHasReplyModal(false);
|
||||||
i18n={i18n}
|
onReplyToStory(message, mentions, replyTimestamp, visibleStory);
|
||||||
id="MyStories__views--plural"
|
}}
|
||||||
components={[<strong>{views}</strong>]}
|
onSetSkinTone={onSetSkinTone}
|
||||||
/>
|
onTextTooLong={onTextTooLong}
|
||||||
))}
|
onUseEmoji={onUseEmoji}
|
||||||
{views && replies && ' '}
|
preferredReactionEmoji={preferredReactionEmoji}
|
||||||
{replies &&
|
recentEmojis={recentEmojis}
|
||||||
(replies === 1 ? (
|
renderEmojiPicker={renderEmojiPicker}
|
||||||
<Intl
|
replies={[]}
|
||||||
i18n={i18n}
|
skinTone={skinTone}
|
||||||
id="MyStories__replies--singular"
|
storyPreviewAttachment={attachment}
|
||||||
components={[<strong>{replies}</strong>]}
|
views={[]}
|
||||||
/>
|
/>
|
||||||
) : (
|
)}
|
||||||
<Intl
|
|
||||||
i18n={i18n}
|
|
||||||
id="MyStories__replies--plural"
|
|
||||||
components={[<strong>{replies}</strong>]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className="StoryViewer__reply"
|
|
||||||
onClick={() => setHasReplyModal(true)}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{i18n('StoryViewer__reply')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{hasReplyModal && (
|
</FocusTrap>
|
||||||
<StoryViewsNRepliesModal
|
|
||||||
authorTitle={title}
|
|
||||||
getPreferredBadge={getPreferredBadge}
|
|
||||||
i18n={i18n}
|
|
||||||
isMyStory={isMe}
|
|
||||||
onClose={() => setHasReplyModal(false)}
|
|
||||||
onReact={emoji => {
|
|
||||||
onReactToStory(emoji, visibleStory);
|
|
||||||
}}
|
|
||||||
onReply={(message, mentions, replyTimestamp) => {
|
|
||||||
setHasReplyModal(false);
|
|
||||||
onReplyToStory(message, mentions, replyTimestamp, visibleStory);
|
|
||||||
}}
|
|
||||||
onSetSkinTone={onSetSkinTone}
|
|
||||||
onTextTooLong={onTextTooLong}
|
|
||||||
onUseEmoji={onUseEmoji}
|
|
||||||
preferredReactionEmoji={preferredReactionEmoji}
|
|
||||||
recentEmojis={recentEmojis}
|
|
||||||
renderEmojiPicker={renderEmojiPicker}
|
|
||||||
replies={[]}
|
|
||||||
skinTone={skinTone}
|
|
||||||
storyPreviewAttachment={attachment}
|
|
||||||
views={[]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue