From 06476de6c9ac1d5ed6a5014f71e3d78cb47570ee Mon Sep 17 00:00:00 2001 From: Josh Perez <60019601+josh-signal@users.noreply.github.com> Date: Mon, 25 Jul 2022 14:55:44 -0400 Subject: [PATCH] Adds debugging information to stories --- _locales/en/messages.json | 32 +++ images/icons/v2/copy-outline-24.svg | 1 + stylesheets/components/ContextMenu.scss | 44 ---- stylesheets/components/HueSlider.scss | 52 ++-- stylesheets/components/MediaEditor.scss | 22 +- stylesheets/components/MessageDetail.scss | 3 + stylesheets/components/MyStories.scss | 3 +- stylesheets/components/Stories.scss | 3 +- stylesheets/components/StoryCreator.scss | 18 +- stylesheets/components/StoryDetailsModal.scss | 79 ++++++ stylesheets/components/StoryListItem.scss | 42 ++- stylesheets/components/StoryViewer.scss | 19 +- .../components/StoryViewsNRepliesModal.scss | 27 ++ stylesheets/manifest.scss | 3 +- ts/components/ContextMenu.tsx | 242 ++++++++--------- ts/components/MediaEditor.tsx | 46 ++-- ts/components/MyStories.tsx | 24 +- ts/components/MyStoriesButton.tsx | 2 +- ts/components/Stories.tsx | 2 +- ts/components/StoriesPane.tsx | 14 +- ts/components/StoryCreator.tsx | 24 +- ts/components/StoryDetailsModal.stories.tsx | 91 +++++++ ts/components/StoryDetailsModal.tsx | 244 ++++++++++++++++++ ts/components/StoryListItem.stories.tsx | 2 +- ts/components/StoryListItem.tsx | 88 +++---- ts/components/StoryViewer.stories.tsx | 19 ++ ts/components/StoryViewer.tsx | 207 +++++++++------ .../StoryViewsNRepliesModal.stories.tsx | 147 ++++++----- ts/components/StoryViewsNRepliesModal.tsx | 10 +- ts/components/conversation/Message.tsx | 5 +- ts/components/conversation/MessageDetail.tsx | 28 +- ts/state/ducks/stories.ts | 33 ++- ts/state/selectors/stories.ts | 13 +- ts/state/smart/StoryViewer.tsx | 1 + ts/types/Stories.ts | 14 +- ts/util/getClassNamesFor.ts | 15 +- 36 files changed, 1089 insertions(+), 530 deletions(-) create mode 100644 images/icons/v2/copy-outline-24.svg create mode 100644 stylesheets/components/StoryDetailsModal.scss create mode 100644 ts/components/StoryDetailsModal.stories.tsx create mode 100644 ts/components/StoryDetailsModal.tsx diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 7539eda00..67e8deb29 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -7385,6 +7385,30 @@ "message": "Unmute", "description": "Aria label for unmuting stories" }, + "StoryDetailsModal__sent-time": { + "message": "Sent $time$", + "description": "Sent timestamp", + "placeholders": { + "time": { + "content": "$1", + "example": "Today 5:33pm" + } + } + }, + "StoryDetailsModal__file-size": { + "message": "File size $size$", + "description": "File size description", + "placeholders": { + "size": { + "content": "$1", + "example": "100kb" + } + } + }, + "StoryDetailsModal__copy-timestamp": { + "message": "Copy timestamp", + "description": "Context menu item to help debugging" + }, "StoryViewsNRepliesModal__no-replies": { "message": "No replies yet", "description": "Placeholder text for when there are no replies" @@ -7421,6 +7445,14 @@ "message": "Go to chat", "description": "Label for menu item to go to conversation" }, + "StoryListItem__delete": { + "message": "Delete", + "description": "Label for menu item to delete a story" + }, + "StoryListItem__info": { + "message": "Info", + "description": "Label for menu item to get a story's information" + }, "StoryListItem__hide-modal--body": { "message": "Hide story? New story updates from $name$ wonโ€™t appear at the top of the stories list anymore.", "description": "Body for the confirmation dialog for hiding a story" diff --git a/images/icons/v2/copy-outline-24.svg b/images/icons/v2/copy-outline-24.svg new file mode 100644 index 000000000..cf6e8360e --- /dev/null +++ b/images/icons/v2/copy-outline-24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/stylesheets/components/ContextMenu.scss b/stylesheets/components/ContextMenu.scss index 2c208cf4f..5ac873160 100644 --- a/stylesheets/components/ContextMenu.scss +++ b/stylesheets/components/ContextMenu.scss @@ -20,50 +20,6 @@ &__button { @include button-reset(); - align-items: center; - border-radius: 16px; - display: flex; - height: 32px; - justify-content: center; - opacity: 0.5; - width: 32px; - - &:focus, - &:hover { - opacity: 1; - } - - &::after { - @include light-theme { - @include color-svg( - '../images/icons/v2/collapse-down-20.svg', - $color-black - ); - } - @include dark-theme { - @include color-svg( - '../images/icons/v2/collapse-down-20.svg', - $color-white - ); - } - content: ''; - display: block; - flex-shrink: 0; - height: 24px; - width: 24px; - } - - &--active { - opacity: 1; - - @include light-theme() { - background-color: $color-gray-05; - } - - @include dark-theme() { - background-color: $color-gray-75; - } - } } &__option { diff --git a/stylesheets/components/HueSlider.scss b/stylesheets/components/HueSlider.scss index ec097800c..cce443f95 100644 --- a/stylesheets/components/HueSlider.scss +++ b/stylesheets/components/HueSlider.scss @@ -1,30 +1,34 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -.HueSlider.Slider { - background-image: linear-gradient( - 90deg, - hsl(0, 0%, 0%), - hsl(0, 100%, 50%), - hsl(45, 100%, 50%), - hsl(90, 100%, 50%), - hsl(135, 100%, 50%), - hsl(180, 100%, 50%), - hsl(225, 100%, 50%), - hsl(270, 100%, 50%), - hsl(315, 100%, 50%), - hsl(0, 0%, 100%) - ); - border-radius: 4px; - height: 8px; - margin-left: 7px; - width: 280px; +.HueSlider { + &.Slider { + background-image: linear-gradient( + 90deg, + hsl(0, 0%, 0%), + hsl(0, 100%, 50%), + hsl(45, 100%, 50%), + hsl(90, 100%, 50%), + hsl(135, 100%, 50%), + hsl(180, 100%, 50%), + hsl(225, 100%, 50%), + hsl(270, 100%, 50%), + hsl(315, 100%, 50%), + hsl(0, 0%, 100%) + ); + border-radius: 4px; + height: 8px; + margin-left: 7px; + width: 280px; + } - &__handle.Slider__handle { - border: 7px solid $color-white; - margin-top: -7px; - margin-left: -11px; - height: 22px; - width: 22px; + &__handle { + &.Slider__handle { + border: 7px solid $color-white; + margin-top: -7px; + margin-left: -11px; + height: 22px; + width: 22px; + } } } diff --git a/stylesheets/components/MediaEditor.scss b/stylesheets/components/MediaEditor.scss index 79d0eb1e6..65d700f20 100644 --- a/stylesheets/components/MediaEditor.scss +++ b/stylesheets/components/MediaEditor.scss @@ -152,7 +152,8 @@ margin-bottom: 22px; padding: 14px 12px; - &__tool { + &__tool, + &__tool__button { margin-right: 14px; } @@ -170,6 +171,7 @@ } @include button-reset; + display: flex; margin: 0 8px; padding: 8px; @@ -179,31 +181,31 @@ padding: 0 6px; } - &--draw-pen { + &--draw-pen__button { @include icon('pen-20.svg'); } - &--draw-highlighter { + &--draw-highlighter__button { @include icon('pen-highlighter-20.svg'); } - &--width-thin { + &--width-thin__button { @include icon('pen-light-20.svg'); } - &--width-regular { + &--width-regular__button { @include icon('pen-regular-20.svg'); } - &--width-medium { + &--width-medium__button { @include icon('pen-medium-20.svg'); } - &--width-heavy { + &--width-heavy__button { @include icon('pen-heavy-20.svg'); } - &--text-regular { + &--text-regular__button { @include icon('text-regular-20.svg'); } - &--text-highlight { + &--text-highlight__button { @include icon('text-highlight-20.svg'); } - &--text-outline { + &--text-outline__button { @include icon('text-outline-20.svg'); } &--rotate { diff --git a/stylesheets/components/MessageDetail.scss b/stylesheets/components/MessageDetail.scss index 74534ece9..42e842626 100644 --- a/stylesheets/components/MessageDetail.scss +++ b/stylesheets/components/MessageDetail.scss @@ -28,6 +28,9 @@ min-width: 72px; } +.module-message-detail__unix-timestamp-menu__button { +} + .module-message-detail__unix-timestamp { @include light-theme { color: $color-gray-05; diff --git a/stylesheets/components/MyStories.scss b/stylesheets/components/MyStories.scss index ab58e153e..866bb967a 100644 --- a/stylesheets/components/MyStories.scss +++ b/stylesheets/components/MyStories.scss @@ -56,7 +56,7 @@ } } - &__more { + &__more__button { align-items: center; background: $color-gray-65; border-radius: 100%; @@ -64,7 +64,6 @@ height: 28px; justify-content: center; margin-left: 16px; - opacity: 1; width: 28px; &::after { diff --git a/stylesheets/components/Stories.scss b/stylesheets/components/Stories.scss index ca2fb450e..ad30ced2c 100644 --- a/stylesheets/components/Stories.scss +++ b/stylesheets/components/Stories.scss @@ -21,7 +21,8 @@ width: 380px; padding-top: calc(14px + var(--title-bar-drag-area-height)); - &__settings { + &__settings__button { + margin-left: 24px; opacity: 1; position: absolute; right: 12px; diff --git a/stylesheets/components/StoryCreator.scss b/stylesheets/components/StoryCreator.scss index faeda4800..6e7491376 100644 --- a/stylesheets/components/StoryCreator.scss +++ b/stylesheets/components/StoryCreator.scss @@ -143,21 +143,17 @@ margin-bottom: 22px; padding: 14px 12px; - &__tool { + &__tool, + &__tool__button { margin-right: 14px; } &__button { @mixin icon($icon) { @include svg($icon); - opacity: 1; height: 20px; width: 20px; border-radius: 0; - - &::after { - display: none; - } } @include button-reset; @@ -173,19 +169,19 @@ &--bg-none { @include icon('text-effect-off-24.svg'); } - &--font-regular { + &--font-regular__button { @include icon('font-regular.svg'); } - &--font-bold { + &--font-bold__button { @include icon('font-bold.svg'); } - &--font-serif { + &--font-serif__button { @include icon('font-serif.svg'); } - &--font-script { + &--font-script__button { @include icon('font-script.svg'); } - &--font-condensed { + &--font-condensed__button { @include icon('font-condensed.svg'); } } diff --git a/stylesheets/components/StoryDetailsModal.scss b/stylesheets/components/StoryDetailsModal.scss new file mode 100644 index 000000000..44072690b --- /dev/null +++ b/stylesheets/components/StoryDetailsModal.scss @@ -0,0 +1,79 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.StoryDetailsModal { + min-width: 320px; + overflow: hidden; + + &__overlay-container { + align-items: flex-end; + justify-content: flex-end; + } + + &__debugger__button { + color: $color-gray-25; + display: block; + font-weight: 600; + height: auto; + width: auto; + + &__text { + font-weight: normal; + } + } + + &__copy-icon { + @include dark-theme { + @include color-svg( + '../images/icons/v2/copy-outline-24.svg', + $color-white + ); + } + @include light-theme { + @include color-svg( + '../images/icons/v2/copy-outline-24.svg', + $color-black + ); + } + } + + &__contact-container { + border-top: 1px solid $color-gray-75; + } + + &__contact-group__header { + @include font-body-1-bold; + align-items: center; + display: flex; + justify-content: space-between; + margin-top: 24px; + padding: 10px 0; + user-select: none; + + &:first-child { + margin-top: 0; + } + } + + &__contact { + margin-bottom: 8px; + padding: 8px 0; + display: flex; + flex-direction: row; + align-items: center; + + &__text { + @include font-body-1; + flex-grow: 1; + margin-left: 10px; + } + + &:last-child { + margin-bottom: 0; + } + } + + &__status-timestamp { + margin-left: 6px; + } +} diff --git a/stylesheets/components/StoryListItem.scss b/stylesheets/components/StoryListItem.scss index c25f81b23..d29c526af 100644 --- a/stylesheets/components/StoryListItem.scss +++ b/stylesheets/components/StoryListItem.scss @@ -2,24 +2,26 @@ // SPDX-License-Identifier: AGPL-3.0-only .StoryListItem { - @include button-reset; - align-items: center; - border-radius: 10px; - display: flex; - height: 96px; - padding: 0 10px; - width: 100%; + &__button { + @include button-reset; + align-items: center; + border-radius: 10px; + display: flex; + height: 96px; + padding: 0 10px; + width: 100%; - @include keyboard-mode { - &:focus { + @include keyboard-mode { + &:focus { + background: $color-gray-65; + } + } + + &:hover { background: $color-gray-65; } } - &:hover { - background: $color-gray-65; - } - &__info { display: flex; flex: 1; @@ -107,8 +109,22 @@ @include color-svg('../images/icons/v2/open-24.svg', $color-white); } + &--delete { + @include color-svg( + '../images/icons/v2/trash-outline-24.svg', + $color-white + ); + } + &--hide { @include color-svg('../images/icons/v2/x-24.svg', $color-white); } + + &--info { + @include color-svg( + '../images/icons/v2/info-outline-24.svg', + $color-white + ); + } } } diff --git a/stylesheets/components/StoryViewer.scss b/stylesheets/components/StoryViewer.scss index 0e6508c74..4643b6e4a 100644 --- a/stylesheets/components/StoryViewer.scss +++ b/stylesheets/components/StoryViewer.scss @@ -146,16 +146,23 @@ } } - &__more { - @include button-reset; + &__more__button { + display: flex; + justify-content: center; + align-items: center; height: 24px; width: 24px; - @include color-svg('../images/icons/v2/more-horiz-24.svg', $color-white); + &::after { + @include color-svg('../images/icons/v2/more-horiz-24.svg', $color-white); + content: ''; + height: 20px; + width: 20px; - @include keyboard-mode { - &:focus { - background-color: $color-black; + @include keyboard-mode { + &:focus { + background-color: $color-black; + } } } } diff --git a/stylesheets/components/StoryViewsNRepliesModal.scss b/stylesheets/components/StoryViewsNRepliesModal.scss index d6a60e64f..38ea5251c 100644 --- a/stylesheets/components/StoryViewsNRepliesModal.scss +++ b/stylesheets/components/StoryViewsNRepliesModal.scss @@ -180,6 +180,33 @@ width: 40px; } } + + &__debugger__button { + color: $color-gray-25; + display: block; + font-weight: 600; + height: auto; + opacity: 1; + width: auto; + + &--active { + @include dark-theme { + background: inherit; + } + } + + &::after { + display: none; + } + + &__text { + font-weight: normal; + } + } + + &__copy-icon { + @include color-svg('../images/icons/v2/copy-outline-24.svg', $color-white); + } } .Tabs.StoryViewsNRepliesModal__tabs { diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index c9f4347ca..1e831630a 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -108,12 +108,13 @@ @import './components/StagedLinkPreview.scss'; @import './components/Stories.scss'; @import './components/StoryCreator.scss'; +@import './components/StoryDetailsModal.scss'; @import './components/StoryImage.scss'; @import './components/StoryListItem.scss'; @import './components/StoryReplyQuote.scss'; @import './components/StoriesSettingsModal.scss'; -@import './components/StoryViewsNRepliesModal.scss'; @import './components/StoryViewer.scss'; +@import './components/StoryViewsNRepliesModal.scss'; @import './components/SystemMessage.scss'; @import './components/Tabs.scss'; @import './components/TextAttachment.scss'; diff --git a/ts/components/ContextMenu.tsx b/ts/components/ContextMenu.tsx index befcbe7f5..2e5af15df 100644 --- a/ts/components/ContextMenu.tsx +++ b/ts/components/ContextMenu.tsx @@ -1,7 +1,7 @@ // Copyright 2018-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { CSSProperties, KeyboardEvent } from 'react'; +import type { KeyboardEvent, ReactNode } from 'react'; import type { Options } from '@popperjs/core'; import FocusTrap from 'focus-trap-react'; import React, { useEffect, useState } from 'react'; @@ -11,9 +11,10 @@ import { noop } from 'lodash'; import type { Theme } from '../util/theme'; import type { LocalizerType } from '../types/Util'; +import { getClassNamesFor } from '../util/getClassNamesFor'; import { themeClassName } from '../util/theme'; -type OptionType = { +export type ContextMenuOptionType = { readonly description?: string; readonly icon?: string; readonly label: string; @@ -21,47 +22,53 @@ type OptionType = { readonly value?: T; }; -export type ContextMenuPropsType = { - readonly focusedIndex?: number; - readonly isMenuShowing: boolean; - readonly menuOptions: ReadonlyArray>; - readonly onClose: () => unknown; +export type PropsType = { + readonly children?: ReactNode; + readonly i18n: LocalizerType; + readonly menuOptions: ReadonlyArray>; + readonly moduleClassName?: string; + readonly onClick?: () => unknown; + readonly onMenuShowingChanged?: (value: boolean) => unknown; readonly popperOptions?: Pick; - readonly referenceElement: HTMLElement | null; readonly theme?: Theme; readonly title?: string; readonly value?: T; }; -export type PropsType = { - readonly buttonClassName?: string; - readonly buttonStyle?: CSSProperties; - readonly i18n: LocalizerType; -} & Pick< - ContextMenuPropsType, - 'menuOptions' | 'popperOptions' | 'theme' | 'title' | 'value' ->; - -export function ContextMenuPopper({ +export function ContextMenu({ + children, + i18n, menuOptions, - focusedIndex, - isMenuShowing, + moduleClassName, + onClick, + onMenuShowingChanged, popperOptions, - onClose, - referenceElement, - title, theme, + title, value, -}: ContextMenuPropsType): JSX.Element | null { +}: PropsType): JSX.Element { + const [isMenuShowing, setIsMenuShowing] = useState(false); + const [focusedIndex, setFocusedIndex] = useState( + undefined + ); const [popperElement, setPopperElement] = useState( null ); + const [referenceElement, setReferenceElement] = + useState(null); + const { styles, attributes } = usePopper(referenceElement, popperElement, { placement: 'top-start', strategy: 'fixed', ...popperOptions, }); + useEffect(() => { + if (onMenuShowingChanged) { + onMenuShowingChanged(isMenuShowing); + } + }, [isMenuShowing, onMenuShowingChanged]); + useEffect(() => { if (!isMenuShowing) { return noop; @@ -69,7 +76,7 @@ export function ContextMenuPopper({ const handleOutsideClick = (event: MouseEvent) => { if (!referenceElement?.contains(event.target as Node)) { - onClose(); + setIsMenuShowing(false); event.stopPropagation(); event.preventDefault(); } @@ -79,92 +86,10 @@ export function ContextMenuPopper({ return () => { document.removeEventListener('click', handleOutsideClick); }; - }, [isMenuShowing, onClose, referenceElement]); - - if (!isMenuShowing) { - return null; - } - - return ( - -
-
- {title &&
{title}
} - {menuOptions.map((option, index) => ( - - ))} -
-
-
- ); -} - -export function ContextMenu({ - buttonClassName, - buttonStyle, - i18n, - menuOptions, - popperOptions, - theme, - title, - value, -}: PropsType): JSX.Element { - const [menuShowing, setMenuShowing] = useState(false); - const [focusedIndex, setFocusedIndex] = useState( - undefined - ); + }, [isMenuShowing, referenceElement]); const handleKeyDown = (ev: KeyboardEvent) => { - if (!menuShowing) { + if (!isMenuShowing) { if (ev.key === 'Enter') { setFocusedIndex(0); } @@ -194,46 +119,101 @@ export function ContextMenu({ const focusedOption = menuOptions[focusedIndex]; focusedOption.onClick(focusedOption.value); } - setMenuShowing(false); + setIsMenuShowing(false); ev.stopPropagation(); ev.preventDefault(); } }; const handleClick = (ev: KeyboardEvent | React.MouseEvent) => { - setMenuShowing(true); + setIsMenuShowing(true); ev.stopPropagation(); ev.preventDefault(); }; - const [referenceElement, setReferenceElement] = - useState(null); + const getClassName = getClassNamesFor('ContextMenu', moduleClassName); return (
+ {isMenuShowing && ( + +
+
+ {title &&
{title}
} + {menuOptions.map((option, index) => ( + + ))} +
+
+
)}
); diff --git a/ts/components/MediaEditor.tsx b/ts/components/MediaEditor.tsx index 957464c91..18f69730d 100644 --- a/ts/components/MediaEditor.tsx +++ b/ts/components/MediaEditor.tsx @@ -569,14 +569,6 @@ export const MediaEditor = ({ value={sliderValue} /> @@ -628,11 +628,6 @@ export const MediaEditor = ({ value={sliderValue} /> diff --git a/ts/components/MyStories.tsx b/ts/components/MyStories.tsx index 6728d38e0..a3e8b15cc 100644 --- a/ts/components/MyStories.tsx +++ b/ts/components/MyStories.tsx @@ -83,7 +83,10 @@ export const MyStories = ({ aria-label={i18n('MyStories__story')} className="MyStories__story__preview" onClick={() => - viewStory(story.messageId, StoryViewModeType.Single) + viewStory({ + storyId: story.messageId, + storyViewMode: StoryViewModeType.Single, + }) } type="button" > @@ -120,9 +123,15 @@ export const MyStories = ({ type="button" /> { + onForward(story.messageId); + }, + }, { icon: 'MyStories__icon--save', label: i18n('save'), @@ -131,10 +140,14 @@ export const MyStories = ({ }, }, { - icon: 'MyStories__icon--forward', - label: i18n('forward'), + icon: 'StoryListItem__icon--info', + label: i18n('StoryListItem__info'), onClick: () => { - onForward(story.messageId); + viewStory({ + storyId: story.messageId, + storyViewMode: StoryViewModeType.Single, + shouldShowDetailsModal: true, + }); }, }, { @@ -145,6 +158,7 @@ export const MyStories = ({ }, }, ]} + moduleClassName="MyStories__story__more" theme={Theme.Dark} /> diff --git a/ts/components/MyStoriesButton.tsx b/ts/components/MyStoriesButton.tsx index cf67a1566..d2960ea7d 100644 --- a/ts/components/MyStoriesButton.tsx +++ b/ts/components/MyStoriesButton.tsx @@ -42,7 +42,7 @@ export const MyStoriesButton = ({
diff --git a/ts/components/StoriesPane.tsx b/ts/components/StoriesPane.tsx index f3f9be6a2..58953f36d 100644 --- a/ts/components/StoriesPane.tsx +++ b/ts/components/StoriesPane.tsx @@ -66,12 +66,12 @@ export type PropsType = { onAddStory: () => unknown; onMyStoriesClicked: () => unknown; onStoriesSettings: () => unknown; - onStoryClicked: (conversationId: string) => unknown; queueStoryDownload: (storyId: string) => unknown; showConversation: ShowConversationType; stories: Array; toggleHideStories: (conversationId: string) => unknown; toggleStoriesView: () => unknown; + viewUserStories: (conversationId: string) => unknown; }; export const StoriesPane = ({ @@ -82,12 +82,12 @@ export const StoriesPane = ({ onAddStory, onMyStoriesClicked, onStoriesSettings, - onStoryClicked, queueStoryDownload, showConversation, stories, toggleHideStories, toggleStoriesView, + viewUserStories, }: PropsType): JSX.Element => { const [searchTerm, setSearchTerm] = useState(''); const [isShowingHiddenStories, setIsShowingHiddenStories] = useState(false); @@ -122,7 +122,6 @@ export const StoriesPane = ({ type="button" /> { - onStoryClicked(story.conversationId); - }} onHideStory={toggleHideStories} onGoToConversation={conversationId => { showConversation({ conversationId }); @@ -176,6 +173,7 @@ export const StoriesPane = ({ }} queueStoryDownload={queueStoryDownload} story={story.storyView} + viewUserStories={viewUserStories} /> ))} {Boolean(hiddenStories.length) && ( @@ -195,9 +193,6 @@ export const StoriesPane = ({ key={story.storyView.timestamp} i18n={i18n} isHidden - onClick={() => { - onStoryClicked(story.conversationId); - }} onHideStory={toggleHideStories} onGoToConversation={conversationId => { showConversation({ conversationId }); @@ -205,6 +200,7 @@ export const StoriesPane = ({ }} queueStoryDownload={queueStoryDownload} story={story.storyView} + viewUserStories={viewUserStories} /> ))} diff --git a/ts/components/StoryCreator.tsx b/ts/components/StoryCreator.tsx index 64b404b28..3673c186f 100644 --- a/ts/components/StoryCreator.tsx +++ b/ts/components/StoryCreator.tsx @@ -267,18 +267,6 @@ export const StoryCreator = ({ value={sliderValue} /> diff --git a/ts/components/StoryDetailsModal.stories.tsx b/ts/components/StoryDetailsModal.stories.tsx new file mode 100644 index 000000000..fb0d7c75c --- /dev/null +++ b/ts/components/StoryDetailsModal.stories.tsx @@ -0,0 +1,91 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { Meta, Story } from '@storybook/react'; +import React from 'react'; +import casual from 'casual'; + +import type { PropsType } from './StoryDetailsModal'; +import enMessages from '../../_locales/en/messages.json'; +import { SendStatus } from '../messages/MessageSendState'; +import { StoryDetailsModal } from './StoryDetailsModal'; +import { fakeAttachment } from '../test-both/helpers/fakeAttachment'; +import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation'; +import { setupI18n } from '../util/setupI18n'; + +const i18n = setupI18n('en', enMessages); + +export default { + title: 'Components/StoryDetailsModal', + component: StoryDetailsModal, + argTypes: { + getPreferredBadge: { action: true }, + i18n: { + defaultValue: i18n, + }, + onClose: { action: true }, + sender: { + defaultValue: getDefaultConversation(), + }, + sendState: { + defaultValue: undefined, + }, + size: { + defaultValue: fakeAttachment().size, + }, + timestamp: { + defaultValue: Date.now(), + }, + }, +} as Meta; + +const Template: Story = args => ; + +export const MyStory = Template.bind({}); +MyStory.args = { + sendState: [ + { + recipient: getDefaultConversation(), + status: SendStatus.Delivered, + updatedAt: casual.unix_time, + }, + { + recipient: getDefaultConversation(), + status: SendStatus.Delivered, + updatedAt: casual.unix_time, + }, + { + recipient: getDefaultConversation(), + status: SendStatus.Delivered, + updatedAt: casual.unix_time, + }, + { + recipient: getDefaultConversation(), + status: SendStatus.Delivered, + updatedAt: casual.unix_time, + }, + { + recipient: getDefaultConversation(), + status: SendStatus.Sent, + updatedAt: casual.unix_time, + }, + { + recipient: getDefaultConversation(), + status: SendStatus.Viewed, + updatedAt: casual.unix_time, + }, + { + recipient: getDefaultConversation(), + status: SendStatus.Viewed, + updatedAt: casual.unix_time, + }, + { + recipient: getDefaultConversation(), + status: SendStatus.Viewed, + updatedAt: casual.unix_time, + }, + ], +}; + +export const OtherStory = Template.bind({}); +OtherStory.args = {}; diff --git a/ts/components/StoryDetailsModal.tsx b/ts/components/StoryDetailsModal.tsx new file mode 100644 index 000000000..ac5ed3634 --- /dev/null +++ b/ts/components/StoryDetailsModal.tsx @@ -0,0 +1,244 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import formatFileSize from 'filesize'; +import type { LocalizerType } from '../types/Util'; +import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; +import type { StorySendStateType, StoryViewType } from '../types/Stories'; +import { Avatar, AvatarSize } from './Avatar'; +import { ContactName } from './conversation/ContactName'; +import { ContextMenu } from './ContextMenu'; +import { Intl } from './Intl'; +import { Modal } from './Modal'; +import { SendStatus } from '../messages/MessageSendState'; +import { Theme } from '../util/theme'; +import { ThemeType } from '../types/Util'; +import { Time } from './Time'; +import { formatDateTimeLong } from '../util/timestamp'; +import { groupBy } from '../util/mapUtil'; + +export type PropsType = { + getPreferredBadge: PreferredBadgeSelectorType; + i18n: LocalizerType; + onClose: () => unknown; + sender: StoryViewType['sender']; + sendState?: Array; + size?: number; + timestamp: number; +}; + +const contactSortCollator = new window.Intl.Collator(); + +function getI18nKey(sendStatus: SendStatus | undefined): string { + if (sendStatus === SendStatus.Failed) { + return 'MessageDetailsHeader--Failed'; + } + + if (sendStatus === SendStatus.Viewed) { + return 'MessageDetailsHeader--Viewed'; + } + + if (sendStatus === SendStatus.Read) { + return 'MessageDetailsHeader--Read'; + } + + if (sendStatus === SendStatus.Delivered) { + return 'MessageDetailsHeader--Delivered'; + } + + if (sendStatus === SendStatus.Sent) { + return 'MessageDetailsHeader--Sent'; + } + + if (sendStatus === SendStatus.Pending) { + return 'MessageDetailsHeader--Pending'; + } + + return 'from'; +} + +export const StoryDetailsModal = ({ + getPreferredBadge, + i18n, + onClose, + sender, + sendState, + size, + timestamp, +}: PropsType): JSX.Element => { + const contactsBySendStatus = sendState + ? groupBy(sendState, contact => contact.status) + : undefined; + + let content: JSX.Element; + if (contactsBySendStatus) { + content = ( +
+ {[ + SendStatus.Failed, + SendStatus.Viewed, + SendStatus.Read, + SendStatus.Delivered, + SendStatus.Sent, + SendStatus.Pending, + ].map(sendStatus => { + const contacts = contactsBySendStatus.get(sendStatus); + + if (!contacts) { + return null; + } + + const i18nKey = getI18nKey(sendStatus); + + const sortedContacts = [...contacts].sort((a, b) => + contactSortCollator.compare(a.recipient.title, b.recipient.title) + ); + + return ( +
+
+ {i18n(i18nKey)} +
+ {sortedContacts.map(status => { + const contact = status.recipient; + + return ( +
+ +
+ +
+ {status.updatedAt && ( + + )} +
+ ); + })} +
+ ); + })} +
+ ); + } else { + content = ( +
+
+
+ {i18n('sent')} +
+
+ +
+
+ +
+
+ +
+
+
+ ); + } + + return ( + { + window.navigator.clipboard.writeText(String(timestamp)); + }, + }, + ]} + moduleClassName="StoryDetailsModal__debugger" + popperOptions={{ + placement: 'bottom', + strategy: 'absolute', + }} + theme={Theme.Dark} + > +
+ + {formatDateTimeLong(i18n, timestamp)} + , + ]} + /> +
+ {size && ( +
+ + {formatFileSize(size)} + , + ]} + /> +
+ )} +
+ } + > + {content} + + ); +}; diff --git a/ts/components/StoryListItem.stories.tsx b/ts/components/StoryListItem.stories.tsx index 48be6961b..6e3fb5aac 100644 --- a/ts/components/StoryListItem.stories.tsx +++ b/ts/components/StoryListItem.stories.tsx @@ -23,7 +23,6 @@ export default { i18n: { defaultValue: i18n, }, - onClick: { action: true }, onGoToConversation: { action: true }, onHideStory: { action: true }, queueStoryDownload: { action: true }, @@ -34,6 +33,7 @@ export default { timestamp: Date.now(), }, }, + viewUserStories: { action: true }, }, } as Meta; diff --git a/ts/components/StoryListItem.tsx b/ts/components/StoryListItem.tsx index d5c9aab41..f8861a316 100644 --- a/ts/components/StoryListItem.tsx +++ b/ts/components/StoryListItem.tsx @@ -7,7 +7,7 @@ import type { LocalizerType } from '../types/Util'; import type { ConversationStoryType, StoryViewType } from '../types/Stories'; import { Avatar, AvatarSize } from './Avatar'; import { ConfirmationDialog } from './ConfirmationDialog'; -import { ContextMenuPopper } from './ContextMenu'; +import { ContextMenu } from './ContextMenu'; import { HasStories } from '../types/Stories'; import { MessageTimestamp } from './conversation/MessageTimestamp'; import { StoryImage } from './StoryImage'; @@ -15,27 +15,27 @@ import { getAvatarColor } from '../types/Colors'; export type PropsType = Pick & { i18n: LocalizerType; - onClick: () => unknown; onGoToConversation: (conversationId: string) => unknown; onHideStory: (conversationId: string) => unknown; queueStoryDownload: (storyId: string) => unknown; story: StoryViewType; + viewUserStories: ( + conversationId: string, + shouldShowDetailsModal?: boolean + ) => unknown; }; export const StoryListItem = ({ group, i18n, isHidden, - onClick, onGoToConversation, onHideStory, queueStoryDownload, story, + viewUserStories, }: PropsType): JSX.Element => { const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false); - const [isShowingContextMenu, setIsShowingContextMenu] = useState(false); - const [referenceElement, setReferenceElement] = - useState(null); const { attachment, @@ -72,21 +72,42 @@ export const StoryListItem = ({ return ( <> - - { - if (isHidden) { - onHideStory(sender.id); - } else { - setHasConfirmHideStory(true); - } - }, - }, - { - icon: 'StoryListItem__icon--chat', - label: i18n('StoryListItem__go-to-chat'), - onClick: () => { - onGoToConversation(sender.id); - }, - }, - ]} - onClose={() => setIsShowingContextMenu(false)} - popperOptions={{ - placement: 'bottom', - strategy: 'absolute', - }} - referenceElement={referenceElement} - /> +
{hasConfirmHideStory && ( unknown; getPreferredBadge: PreferredBadgeSelectorType; group?: Pick< ConversationType, @@ -74,6 +77,7 @@ export type PropsType = { recentEmojis?: Array; renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element; replyState?: ReplyStateType; + shouldShowDetailsModal?: boolean; showToast: ShowToastActionCreatorType; skinTone?: number; story: StoryViewType; @@ -95,6 +99,7 @@ enum Arrow { export const StoryViewer = ({ currentIndex, + deleteStoryForEveryone, getPreferredBadge, group, hasAllStoriesMuted, @@ -114,6 +119,7 @@ export const StoryViewer = ({ recentEmojis, renderEmojiPicker, replyState, + shouldShowDetailsModal, showToast, skinTone, story, @@ -121,12 +127,14 @@ export const StoryViewer = ({ toggleHasAllStoriesMuted, viewStory, }: PropsType): JSX.Element => { + const [isShowingContextMenu, setIsShowingContextMenu] = + useState(false); const [storyDuration, setStoryDuration] = useState(); - const [isShowingContextMenu, setIsShowingContextMenu] = useState(false); const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false); - const [referenceElement, setReferenceElement] = - useState(null); const [reactionEmoji, setReactionEmoji] = useState(); + const [confirmDeleteStory, setConfirmDeleteStory] = useState< + StoryViewType | undefined + >(); const { attachment, canReply, isHidden, messageId, sendState, timestamp } = story; @@ -143,19 +151,25 @@ export const StoryViewer = ({ title, } = story.sender; - const [hasReplyModal, setHasReplyModal] = useState(false); + const [hasStoryViewsNRepliesModal, setHasStoryViewsNRepliesModal] = + useState(false); + const [hasStoryDetailsModal, setHasStoryDetailsModal] = useState( + Boolean(shouldShowDetailsModal) + ); const onClose = useCallback(() => { - viewStory(); + viewStory({ + closeViewer: true, + }); }, [viewStory]); const onEscape = useCallback(() => { - if (hasReplyModal) { - setHasReplyModal(false); + if (hasStoryViewsNRepliesModal) { + setHasStoryViewsNRepliesModal(false); } else { onClose(); } - }, [hasReplyModal, onClose]); + }, [hasStoryViewsNRepliesModal, onClose]); useEscapeHandling(onEscape); @@ -225,11 +239,11 @@ export const StoryViewer = ({ } if (value === 100) { - viewStory( - story.messageId, + viewStory({ + storyId: story.messageId, storyViewMode, - StoryViewDirectionType.Next - ); + viewDirection: StoryViewDirectionType.Next, + }); } }, }, @@ -263,7 +277,8 @@ export const StoryViewer = ({ const shouldPauseViewing = hasConfirmHideStory || hasExpandedCaption || - hasReplyModal || + hasStoryDetailsModal || + hasStoryViewsNRepliesModal || isShowingContextMenu || pauseStory || Boolean(reactionEmoji); @@ -284,15 +299,19 @@ export const StoryViewer = ({ const navigateStories = useCallback( (ev: KeyboardEvent) => { if (ev.key === 'ArrowRight') { - viewStory(story.messageId, storyViewMode, StoryViewDirectionType.Next); + viewStory({ + storyId: story.messageId, + storyViewMode, + viewDirection: StoryViewDirectionType.Next, + }); ev.preventDefault(); ev.stopPropagation(); } else if (ev.key === 'ArrowLeft') { - viewStory( - story.messageId, + viewStory({ + storyId: story.messageId, storyViewMode, - StoryViewDirectionType.Previous - ); + viewDirection: StoryViewDirectionType.Previous, + }); ev.preventDefault(); ev.stopPropagation(); } @@ -357,10 +376,50 @@ export const StoryViewer = ({ const replyCount = replies.length; const viewCount = views.length; - const shouldShowContextMenu = !sendState; - const hasPrevNextArrows = storyViewMode !== StoryViewModeType.Single; + const contextMenuOptions: ReadonlyArray> = + sendState + ? [ + { + icon: 'StoryListItem__icon--info', + label: i18n('StoryListItem__info'), + onClick: () => setHasStoryDetailsModal(true), + }, + { + icon: 'StoryListItem__icon--delete', + label: i18n('StoryListItem__delete'), + onClick: () => setConfirmDeleteStory(story), + }, + ] + : [ + { + icon: 'StoryListItem__icon--info', + label: i18n('StoryListItem__info'), + onClick: () => setHasStoryDetailsModal(true), + }, + { + icon: 'StoryListItem__icon--hide', + label: isHidden + ? i18n('StoryListItem__unhide') + : i18n('StoryListItem__hide'), + onClick: () => { + if (isHidden) { + onHideStory(id); + } else { + setHasConfirmHideStory(true); + } + }, + }, + { + icon: 'StoryListItem__icon--chat', + label: i18n('StoryListItem__go-to-chat'), + onClick: () => { + onGoToConversation(id); + }, + }, + ]; + return (
@@ -379,11 +438,11 @@ export const StoryViewer = ({ } )} onClick={() => - viewStory( - story.messageId, + viewStory({ + storyId: story.messageId, storyViewMode, - StoryViewDirectionType.Previous - ) + viewDirection: StoryViewDirectionType.Previous, + }) } onMouseMove={() => setArrowToShow(Arrow.Left)} type="button" @@ -519,15 +578,14 @@ export const StoryViewer = ({ onClick={toggleHasAllStoriesMuted} type="button" /> - {shouldShowContextMenu && ( -
@@ -555,14 +613,14 @@ export const StoryViewer = ({ {canReply && (
- { - if (isHidden) { - onHideStory(id); - } else { - setHasConfirmHideStory(true); - } - }, - }, - { - icon: 'StoryListItem__icon--chat', - label: i18n('StoryListItem__go-to-chat'), - onClick: () => { - onGoToConversation(id); - }, - }, - ]} - onClose={() => setIsShowingContextMenu(false)} - referenceElement={referenceElement} - theme={Theme.Dark} - /> - {hasReplyModal && canReply && ( + {hasStoryDetailsModal && ( + setHasStoryDetailsModal(false)} + sender={story.sender} + sendState={sendState} + size={attachment?.size} + timestamp={timestamp} + /> + )} + {hasStoryViewsNRepliesModal && ( setHasReplyModal(false)} + onClose={() => setHasStoryViewsNRepliesModal(false)} onReact={emoji => { onReactToStory(emoji, story); - setHasReplyModal(false); + setHasStoryViewsNRepliesModal(false); setReactionEmoji(emoji); showToast(ToastType.StoryReact); }} onReply={(message, mentions, replyTimestamp) => { if (!isGroupStory) { - setHasReplyModal(false); + setHasStoryViewsNRepliesModal(false); } onReplyToStory(message, mentions, replyTimestamp, story); showToast(ToastType.StoryReply); @@ -712,6 +754,21 @@ export const StoryViewer = ({ {i18n('StoryListItem__hide-modal--body', [String(firstName)])}
)} + {confirmDeleteStory && ( + deleteStoryForEveryone(confirmDeleteStory), + style: 'negative', + }, + ]} + i18n={i18n} + onClose={() => setConfirmDeleteStory(undefined)} + > + {i18n('MyStories__delete')} + + )} ); diff --git a/ts/components/StoryViewsNRepliesModal.stories.tsx b/ts/components/StoryViewsNRepliesModal.stories.tsx index 08c658255..3c6583537 100644 --- a/ts/components/StoryViewsNRepliesModal.stories.tsx +++ b/ts/components/StoryViewsNRepliesModal.stories.tsx @@ -1,8 +1,8 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import type { Meta, Story } from '@storybook/react'; import React from 'react'; -import { action } from '@storybook/addon-actions'; import type { PropsType } from './StoryViewsNRepliesModal'; import * as durations from '../util/durations'; @@ -18,35 +18,50 @@ const i18n = setupI18n('en', enMessages); export default { title: 'Components/StoryViewsNRepliesModal', -}; - -function getDefaultProps(): PropsType { - return { - authorTitle: getDefaultConversation().title, - getPreferredBadge: () => undefined, - i18n, - isMyStory: false, - onClose: action('onClose'), - onSetSkinTone: action('onSetSkinTone'), - onReact: action('onReact'), - onReply: action('onReply'), - onTextTooLong: action('onTextTooLong'), - onUseEmoji: action('onUseEmoji'), - preferredReactionEmoji: ['โค๏ธ', '๐Ÿ‘', '๐Ÿ‘Ž', '๐Ÿ˜‚', '๐Ÿ˜ฎ', '๐Ÿ˜ข'], - renderEmojiPicker: () =>
, - replies: [], - storyPreviewAttachment: fakeAttachment({ - thumbnail: { - contentType: IMAGE_JPEG, - height: 64, - objectUrl: '/fixtures/nathan-anderson-316188-unsplash.jpg', - path: '', - width: 40, - }, - }), - views: [], - }; -} + component: StoryViewsNRepliesModal, + argTypes: { + authorTitle: { + defaultValue: getDefaultConversation().title, + }, + canReply: { + defaultValue: true, + }, + getPreferredBadge: { action: true }, + i18n: { + defaultValue: i18n, + }, + isMyStory: { + defaultValue: false, + }, + onClose: { action: true }, + onSetSkinTone: { action: true }, + onReact: { action: true }, + onReply: { action: true }, + onTextTooLong: { action: true }, + onUseEmoji: { action: true }, + preferredReactionEmoji: { + defaultValue: ['โค๏ธ', '๐Ÿ‘', '๐Ÿ‘Ž', '๐Ÿ˜‚', '๐Ÿ˜ฎ', '๐Ÿ˜ข'], + }, + renderEmojiPicker: { action: true }, + replies: { + defaultValue: [], + }, + storyPreviewAttachment: { + defaultValue: fakeAttachment({ + thumbnail: { + contentType: IMAGE_JPEG, + height: 64, + objectUrl: '/fixtures/nathan-anderson-316188-unsplash.jpg', + path: '', + width: 40, + }, + }), + }, + views: { + defaultValue: [], + }, + }, +} as Meta; function getViewsAndReplies() { const p1 = getDefaultConversation(); @@ -107,47 +122,51 @@ function getViewsAndReplies() { }; } -export const CanReply = (): JSX.Element => ( - +const Template: Story = args => ( + ); -CanReply.story = { - name: 'Can reply', +export const CanReply = Template.bind({}); +CanReply.args = {}; +CanReply.storyName = 'Can reply'; + +export const ViewsOnly = Template.bind({}); +ViewsOnly.args = { + isMyStory: true, + views: getViewsAndReplies().views, }; +ViewsOnly.storyName = 'Views only'; -export const ViewsOnly = (): JSX.Element => ( - -); - -ViewsOnly.story = { - name: 'Views only', +export const InAGroupNoReplies = Template.bind({}); +InAGroupNoReplies.args = { + isGroupStory: true, }; +InAGroupNoReplies.storyName = 'In a group (no replies)'; -export const InAGroupNoReplies = (): JSX.Element => ( - -); - -InAGroupNoReplies.story = { - name: 'In a group (no replies)', -}; - -export const InAGroup = (): JSX.Element => { +export const InAGroup = Template.bind({}); +{ const { views, replies } = getViewsAndReplies(); + InAGroup.args = { + isGroupStory: true, + replies, + views, + }; +} +InAGroup.storyName = 'In a group'; - return ( - - ); +export const CantReply = Template.bind({}); +CantReply.args = { + canReply: false, }; -InAGroup.story = { - name: 'In a group', -}; +export const InAGroupCantReply = Template.bind({}); +{ + const { views, replies } = getViewsAndReplies(); + InAGroupCantReply.args = { + canReply: false, + isGroupStory: true, + replies, + views, + }; +} +InAGroupCantReply.storyName = "In a group (can't reply)"; diff --git a/ts/components/StoryViewsNRepliesModal.tsx b/ts/components/StoryViewsNRepliesModal.tsx index 8beffe363..00c9ddc6c 100644 --- a/ts/components/StoryViewsNRepliesModal.tsx +++ b/ts/components/StoryViewsNRepliesModal.tsx @@ -34,6 +34,7 @@ enum Tab { export type PropsType = { authorTitle: string; + canReply: boolean; getPreferredBadge: PreferredBadgeSelectorType; i18n: LocalizerType; isGroupStory?: boolean; @@ -59,6 +60,7 @@ export type PropsType = { export const StoryViewsNRepliesModal = ({ authorTitle, + canReply, getPreferredBadge, i18n, isGroupStory, @@ -76,7 +78,7 @@ export const StoryViewsNRepliesModal = ({ skinTone, storyPreviewAttachment, views, -}: PropsType): JSX.Element => { +}: PropsType): JSX.Element | null => { const inputApiRef = useRef(); const [bottom, setBottom] = useState(null); const [messageBodyText, setMessageBodyText] = useState(''); @@ -117,7 +119,7 @@ export const StoryViewsNRepliesModal = ({ let composerElement: JSX.Element | undefined; - if (!isMyStory) { + if (!isMyStory && canReply) { composerElement = ( <> {!isGroupStory && ( @@ -373,6 +375,10 @@ export const StoryViewsNRepliesModal = ({ ) : undefined; + if (!tabsElement && !viewsElement && !repliesElement && !composerElement) { + return null; + } + return ( { isViewOnce={false} moduleClassName="StoryReplyQuote" onClick={() => { - viewStory(storyReplyContext.storyId, StoryViewModeType.Single); + viewStory({ + storyId: storyReplyContext.storyId, + storyViewMode: StoryViewModeType.Single, + }); }} rawAttachment={storyReplyContext.rawAttachment} reactionEmoji={storyReplyContext.emoji} diff --git a/ts/components/conversation/MessageDetail.tsx b/ts/components/conversation/MessageDetail.tsx index 6136a1034..9d8113691 100644 --- a/ts/components/conversation/MessageDetail.tsx +++ b/ts/components/conversation/MessageDetail.tsx @@ -8,6 +8,7 @@ import { noop } from 'lodash'; import { Avatar, AvatarSize } from '../Avatar'; import { ContactName } from './ContactName'; +import { ContextMenu } from '../ContextMenu'; import { Time } from '../Time'; import type { Props as MessagePropsType, @@ -392,12 +393,27 @@ export class MessageDetail extends React.Component { {i18n('sent')} - {' '} - - ({sentAt}) - + { + window.navigator.clipboard.writeText(String(sentAt)); + }, + }, + ]} + > + <> + {' '} + + ({sentAt}) + + + {receivedAt && message.direction === 'incoming' ? ( diff --git a/ts/state/ducks/stories.ts b/ts/state/ducks/stories.ts index b9d563b2a..69db3f38a 100644 --- a/ts/state/ducks/stories.ts +++ b/ts/state/ducks/stories.ts @@ -63,6 +63,7 @@ export type StoryDataType = { export type SelectedStoryDataType = { currentIndex: number; numStories: number; + shouldShowDetailsModal: boolean; story: StoryDataType; }; @@ -616,7 +617,8 @@ const getSelectedStoryDataForConversationId = ( }; function viewUserStories( - conversationId: string + conversationId: string, + shouldShowDetailsModal = false ): ThunkAction { return (dispatch, getState) => { const { currentIndex, hasUnread, numStories, storiesByConversationId } = @@ -630,6 +632,7 @@ function viewUserStories( selectedStoryData: { currentIndex, numStories, + shouldShowDetailsModal, story, }, storyViewMode: hasUnread @@ -640,19 +643,23 @@ function viewUserStories( }; } -export type ViewStoryActionCreatorType = ( - storyId?: string, - storyViewMode?: StoryViewModeType, - viewDirection?: StoryViewDirectionType -) => unknown; +export type ViewStoryActionCreatorType = (opts: { + closeViewer?: boolean; + storyId?: string; + storyViewMode?: StoryViewModeType; + viewDirection?: StoryViewDirectionType; + shouldShowDetailsModal?: boolean; +}) => unknown; -const viewStory: ViewStoryActionCreatorType = ( +const viewStory: ViewStoryActionCreatorType = ({ + closeViewer, + shouldShowDetailsModal = false, storyId, storyViewMode, - viewDirection -): ThunkAction => { + viewDirection, +}): ThunkAction => { return (dispatch, getState) => { - if (!storyId || !storyViewMode) { + if (closeViewer || !storyId || !storyViewMode) { dispatch({ type: VIEW_STORY, payload: undefined, @@ -691,6 +698,7 @@ const viewStory: ViewStoryActionCreatorType = ( selectedStoryData: { currentIndex, numStories, + shouldShowDetailsModal, story, }, storyViewMode, @@ -713,6 +721,7 @@ const viewStory: ViewStoryActionCreatorType = ( selectedStoryData: { currentIndex: nextIndex, numStories, + shouldShowDetailsModal: false, story: nextStory, }, storyViewMode, @@ -732,6 +741,7 @@ const viewStory: ViewStoryActionCreatorType = ( selectedStoryData: { currentIndex: nextIndex, numStories, + shouldShowDetailsModal: false, story: nextStory, }, storyViewMode, @@ -759,6 +769,7 @@ const viewStory: ViewStoryActionCreatorType = ( selectedStoryData: { currentIndex: nextSelectedStoryData.currentIndex, numStories: nextSelectedStoryData.numStories, + shouldShowDetailsModal: false, story: unreadStory, }, storyViewMode, @@ -819,6 +830,7 @@ const viewStory: ViewStoryActionCreatorType = ( selectedStoryData: { currentIndex: 0, numStories: nextSelectedStoryData.numStories, + shouldShowDetailsModal: false, story: nextSelectedStoryData.storiesByConversationId[0], }, storyViewMode, @@ -855,6 +867,7 @@ const viewStory: ViewStoryActionCreatorType = ( selectedStoryData: { currentIndex: 0, numStories: nextSelectedStoryData.numStories, + shouldShowDetailsModal: false, story: nextSelectedStoryData.storiesByConversationId[0], }, storyViewMode, diff --git a/ts/state/selectors/stories.ts b/ts/state/selectors/stories.ts index aeccdd7b5..523374062 100644 --- a/ts/state/selectors/stories.ts +++ b/ts/state/selectors/stories.ts @@ -101,6 +101,7 @@ export function getStoryView( const sender = pick(conversationSelector(story.sourceUuid || story.source), [ 'acceptedMessageRequest', 'avatarPath', + 'badges', 'color', 'firstName', 'hideStory', @@ -132,17 +133,7 @@ export function getStoryView( innerSendState.push({ ...recipientSendState, - recipient: pick(recipient, [ - 'acceptedMessageRequest', - 'avatarPath', - 'color', - 'id', - 'isMe', - 'name', - 'profileName', - 'sharedGroupNames', - 'title', - ]), + recipient, }); }); diff --git a/ts/state/smart/StoryViewer.tsx b/ts/state/smart/StoryViewer.tsx index 5e98e64f4..274b563ab 100644 --- a/ts/state/smart/StoryViewer.tsx +++ b/ts/state/smart/StoryViewer.tsx @@ -106,6 +106,7 @@ export function SmartStoryViewer(): JSX.Element | null { recentEmojis={recentEmojis} renderEmojiPicker={renderEmojiPicker} replyState={replyState} + shouldShowDetailsModal={selectedStoryData.shouldShowDetailsModal} showToast={showToast} skinTone={skinTone} story={storyView} diff --git a/ts/types/Stories.ts b/ts/types/Stories.ts index 17571fff7..2b4efbc7e 100644 --- a/ts/types/Stories.ts +++ b/ts/types/Stories.ts @@ -52,18 +52,7 @@ export type ConversationStoryType = { export type StorySendStateType = { isAllowedToReplyToStory?: boolean; - recipient: Pick< - ConversationType, - | 'acceptedMessageRequest' - | 'avatarPath' - | 'color' - | 'id' - | 'isMe' - | 'name' - | 'profileName' - | 'sharedGroupNames' - | 'title' - >; + recipient: ConversationType; status: SendStatus; updatedAt?: number; }; @@ -80,6 +69,7 @@ export type StoryViewType = { ConversationType, | 'acceptedMessageRequest' | 'avatarPath' + | 'badges' | 'color' | 'firstName' | 'id' diff --git a/ts/util/getClassNamesFor.ts b/ts/util/getClassNamesFor.ts index 89547fe54..178c0b4aa 100644 --- a/ts/util/getClassNamesFor.ts +++ b/ts/util/getClassNamesFor.ts @@ -7,11 +7,16 @@ export function getClassNamesFor( ...modules: Array ): (modifier?: string) => string { return modifier => { - const cx = modules.map(parentModule => - parentModule && modifier !== undefined - ? `${parentModule}${modifier}` - : undefined - ); + if (modifier === undefined) { + return ''; + } + + const cx = modules + .flatMap(m => (m ? m.split(' ') : undefined)) + .map(parentModule => + parentModule ? `${parentModule}${modifier}` : undefined + ); + return classNames(cx); }; }