Adds captions in the viewer
This commit is contained in:
parent
247149c58e
commit
4015259def
4 changed files with 106 additions and 32 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
? () => {
|
||||
|
|
34
ts/util/graphemeAwareSlice.ts
Normal file
34
ts/util/graphemeAwareSlice.ts
Normal 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,
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue