Adds captions in the viewer

This commit is contained in:
Josh Perez 2022-04-14 13:02:12 -04:00 committed by GitHub
parent 247149c58e
commit 4015259def
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 106 additions and 32 deletions

View file

@ -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;
}

View file

@ -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<boolean>(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 && (
<div className="StoryViewer__caption__overlay" />
)}
<div className="StoryViewer__meta">
{caption && (
<div className="StoryViewer__caption">
{caption.text}
{caption.hasReadMore && !hasExpandedCaption && (
<button
className="MessageBody__read-more"
onClick={() => {
setHasExpandedCaption(true);
}}
onKeyDown={(ev: React.KeyboardEvent) => {
if (ev.key === 'Space' || ev.key === 'Enter') {
setHasExpandedCaption(true);
}
}}
type="button"
>
...
{i18n('MessageBody--read-more')}
</button>
)}
</div>
)}
<Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath}

View file

@ -5,6 +5,7 @@ import React from 'react';
import type { Props as MessageBodyPropsType } from './MessageBody';
import { MessageBody } from './MessageBody';
import { graphemeAwareSlice } from '../../util/graphemeAwareSlice';
export type Props = Pick<
MessageBodyPropsType,
@ -29,37 +30,6 @@ export function doesMessageBodyOverflow(str: string): boolean {
return str.length > 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
? () => {

View file

@ -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,
};
}