Adds captions in the viewer
This commit is contained in:
parent
247149c58e
commit
4015259def
|
@ -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 {
|
&__actions {
|
||||||
margin: 16px 0 32px 0;
|
margin: 16px 0 32px 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { StoryImage } from './StoryImage';
|
||||||
import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal';
|
import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal';
|
||||||
import { getAvatarColor } from '../types/Colors';
|
import { getAvatarColor } from '../types/Colors';
|
||||||
import { getStoryDuration } from '../util/getStoryDuration';
|
import { getStoryDuration } from '../util/getStoryDuration';
|
||||||
|
import { graphemeAwareSlice } from '../util/graphemeAwareSlice';
|
||||||
import { isDownloaded, isDownloading } from '../types/Attachment';
|
import { isDownloaded, isDownloading } from '../types/Attachment';
|
||||||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||||
|
|
||||||
|
@ -48,6 +49,10 @@ export type PropsType = {
|
||||||
views?: number;
|
views?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CAPTION_BUFFER = 20;
|
||||||
|
const CAPTION_INITIAL_LENGTH = 200;
|
||||||
|
const CAPTION_MAX_LENGTH = 700;
|
||||||
|
|
||||||
export const StoryViewer = ({
|
export const StoryViewer = ({
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
group,
|
group,
|
||||||
|
@ -99,6 +104,26 @@ export const StoryViewer = ({
|
||||||
|
|
||||||
useEscapeHandling(onEscape);
|
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
|
// Either we show the next story in the current user's stories or we ask
|
||||||
// for the next user's stories.
|
// for the next user's stories.
|
||||||
const showNextStory = useCallback(() => {
|
const showNextStory = useCallback(() => {
|
||||||
|
@ -242,7 +267,32 @@ export const StoryViewer = ({
|
||||||
queueStoryDownload={queueStoryDownload}
|
queueStoryDownload={queueStoryDownload}
|
||||||
storyId={messageId}
|
storyId={messageId}
|
||||||
/>
|
/>
|
||||||
|
{hasExpandedCaption && (
|
||||||
|
<div className="StoryViewer__caption__overlay" />
|
||||||
|
)}
|
||||||
<div className="StoryViewer__meta">
|
<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
|
<Avatar
|
||||||
acceptedMessageRequest={acceptedMessageRequest}
|
acceptedMessageRequest={acceptedMessageRequest}
|
||||||
avatarPath={avatarPath}
|
avatarPath={avatarPath}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import React from 'react';
|
||||||
|
|
||||||
import type { Props as MessageBodyPropsType } from './MessageBody';
|
import type { Props as MessageBodyPropsType } from './MessageBody';
|
||||||
import { MessageBody } from './MessageBody';
|
import { MessageBody } from './MessageBody';
|
||||||
|
import { graphemeAwareSlice } from '../../util/graphemeAwareSlice';
|
||||||
|
|
||||||
export type Props = Pick<
|
export type Props = Pick<
|
||||||
MessageBodyPropsType,
|
MessageBodyPropsType,
|
||||||
|
@ -29,37 +30,6 @@ export function doesMessageBodyOverflow(str: string): boolean {
|
||||||
return str.length > INITIAL_LENGTH + BUFFER;
|
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({
|
export function MessageBodyReadMore({
|
||||||
bodyRanges,
|
bodyRanges,
|
||||||
direction,
|
direction,
|
||||||
|
@ -74,7 +44,11 @@ export function MessageBodyReadMore({
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
const maxLength = displayLimit || INITIAL_LENGTH;
|
const maxLength = displayLimit || INITIAL_LENGTH;
|
||||||
|
|
||||||
const { hasReadMore, text: slicedText } = graphemeAwareSlice(text, maxLength);
|
const { hasReadMore, text: slicedText } = graphemeAwareSlice(
|
||||||
|
text,
|
||||||
|
maxLength,
|
||||||
|
BUFFER
|
||||||
|
);
|
||||||
|
|
||||||
const onIncreaseTextLength = hasReadMore
|
const onIncreaseTextLength = hasReadMore
|
||||||
? () => {
|
? () => {
|
||||||
|
|
|
@ -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 New Issue