From 4015259def9d6c51a11d0af972a35991becf176a Mon Sep 17 00:00:00 2001 From: Josh Perez <60019601+josh-signal@users.noreply.github.com> Date: Thu, 14 Apr 2022 13:02:12 -0400 Subject: [PATCH] Adds captions in the viewer --- stylesheets/components/StoryViewer.scss | 16 ++++++ ts/components/StoryViewer.tsx | 50 +++++++++++++++++++ .../conversation/MessageBodyReadMore.tsx | 38 +++----------- ts/util/graphemeAwareSlice.ts | 34 +++++++++++++ 4 files changed, 106 insertions(+), 32 deletions(-) create mode 100644 ts/util/graphemeAwareSlice.ts diff --git a/stylesheets/components/StoryViewer.scss b/stylesheets/components/StoryViewer.scss index c784fbc54..710a46990 100644 --- a/stylesheets/components/StoryViewer.scss +++ b/stylesheets/components/StoryViewer.scss @@ -90,6 +90,22 @@ } } + &__caption { + @include font-body-1-bold; + color: $color-gray-05; + padding: 4px 0; + + &__overlay { + background: $color-black-alpha-60; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + z-index: $z-index-base; + } + } + &__actions { margin: 16px 0 32px 0; } diff --git a/ts/components/StoryViewer.tsx b/ts/components/StoryViewer.tsx index 46ed6cff3..9a1128d82 100644 --- a/ts/components/StoryViewer.tsx +++ b/ts/components/StoryViewer.tsx @@ -17,6 +17,7 @@ import { StoryImage } from './StoryImage'; import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal'; import { getAvatarColor } from '../types/Colors'; import { getStoryDuration } from '../util/getStoryDuration'; +import { graphemeAwareSlice } from '../util/graphemeAwareSlice'; import { isDownloaded, isDownloading } from '../types/Attachment'; import { useEscapeHandling } from '../hooks/useEscapeHandling'; @@ -48,6 +49,10 @@ export type PropsType = { views?: number; }; +const CAPTION_BUFFER = 20; +const CAPTION_INITIAL_LENGTH = 200; +const CAPTION_MAX_LENGTH = 700; + export const StoryViewer = ({ getPreferredBadge, group, @@ -99,6 +104,26 @@ export const StoryViewer = ({ useEscapeHandling(onEscape); + // Caption related hooks + const [hasExpandedCaption, setHasExpandedCaption] = useState(false); + + const caption = useMemo(() => { + if (!attachment?.caption) { + return; + } + + return graphemeAwareSlice( + attachment.caption, + hasExpandedCaption ? CAPTION_MAX_LENGTH : CAPTION_INITIAL_LENGTH, + CAPTION_BUFFER + ); + }, [attachment?.caption, hasExpandedCaption]); + + // Reset expansion if messageId changes + useEffect(() => { + setHasExpandedCaption(false); + }, [messageId]); + // Either we show the next story in the current user's stories or we ask // for the next user's stories. const showNextStory = useCallback(() => { @@ -242,7 +267,32 @@ export const StoryViewer = ({ queueStoryDownload={queueStoryDownload} storyId={messageId} /> + {hasExpandedCaption && ( +
+ )}
+ {caption && ( +
+ {caption.text} + {caption.hasReadMore && !hasExpandedCaption && ( + + )} +
+ )} INITIAL_LENGTH + BUFFER; } -function graphemeAwareSlice( - str: string, - length: number -): { - hasReadMore: boolean; - text: string; -} { - if (str.length <= length + BUFFER) { - return { text: str, hasReadMore: false }; - } - - let text: string | undefined; - - for (const { index } of new Intl.Segmenter().segment(str)) { - if (!text && index >= length) { - text = str.slice(0, index); - } - if (text && index > length) { - return { - text, - hasReadMore: true, - }; - } - } - - return { - text: str, - hasReadMore: false, - }; -} - export function MessageBodyReadMore({ bodyRanges, direction, @@ -74,7 +44,11 @@ export function MessageBodyReadMore({ }: Props): JSX.Element { const maxLength = displayLimit || INITIAL_LENGTH; - const { hasReadMore, text: slicedText } = graphemeAwareSlice(text, maxLength); + const { hasReadMore, text: slicedText } = graphemeAwareSlice( + text, + maxLength, + BUFFER + ); const onIncreaseTextLength = hasReadMore ? () => { diff --git a/ts/util/graphemeAwareSlice.ts b/ts/util/graphemeAwareSlice.ts new file mode 100644 index 000000000..04d5ae0b3 --- /dev/null +++ b/ts/util/graphemeAwareSlice.ts @@ -0,0 +1,34 @@ +// Copyright 2021-2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +export function graphemeAwareSlice( + str: string, + length: number, + buffer = 100 +): { + hasReadMore: boolean; + text: string; +} { + if (str.length <= length + buffer) { + return { text: str, hasReadMore: false }; + } + + let text: string | undefined; + + for (const { index } of new Intl.Segmenter().segment(str)) { + if (!text && index >= length) { + text = str.slice(0, index); + } + if (text && index > length) { + return { + text, + hasReadMore: true, + }; + } + } + + return { + text: str, + hasReadMore: false, + }; +}