do not spit url between Read More chunks by expanding chunk to end of url

This commit is contained in:
Patrick Demers 2022-04-16 18:40:22 -05:00 committed by Josh Perez
parent 6ca3452488
commit 5df5cde48c
5 changed files with 142 additions and 38 deletions

View file

@ -44,9 +44,9 @@ import { ToastType } from '../state/ducks/toast';
import { getAvatarColor } from '../types/Colors';
import { getStoryBackground } from '../util/getStoryBackground';
import { getStoryDuration } from '../util/getStoryDuration';
import { graphemeAwareSlice } from '../util/graphemeAwareSlice';
import type { saveAttachment } from '../util/saveAttachment';
import { isVideoAttachment } from '../types/Attachment';
import { graphemeAndLinkAwareSlice } from '../util/graphemeAndLinkAwareSlice';
import { useEscapeHandling } from '../hooks/useEscapeHandling';
import { useRetryStorySend } from '../hooks/useRetryStorySend';
import { resolveStorySendStatus } from '../util/resolveStorySendStatus';
@ -228,7 +228,7 @@ export function StoryViewer({
return;
}
return graphemeAwareSlice(
return graphemeAndLinkAwareSlice(
attachment.caption,
hasExpandedCaption ? CAPTION_MAX_LENGTH : CAPTION_INITIAL_LENGTH,
CAPTION_BUFFER

View file

@ -5,7 +5,7 @@ import React from 'react';
import type { Props as MessageBodyPropsType } from './MessageBody';
import { MessageBody } from './MessageBody';
import { graphemeAwareSlice } from '../../util/graphemeAwareSlice';
import { graphemeAndLinkAwareSlice } from '../../util/graphemeAndLinkAwareSlice';
export type Props = Pick<
MessageBodyPropsType,
@ -46,7 +46,7 @@ export function MessageBodyReadMore({
}: Props): JSX.Element {
const maxLength = displayLimit || INITIAL_LENGTH;
const { hasReadMore, text: slicedText } = graphemeAwareSlice(
const { hasReadMore, text: slicedText } = graphemeAndLinkAwareSlice(
text,
maxLength,
BUFFER

View file

@ -0,0 +1,70 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { graphemeAndLinkAwareSlice } from '../../util/graphemeAndLinkAwareSlice';
describe('graphemeAndLinkAwareSlice', () => {
it('returns entire string when shorter than maximum', () => {
const shortString = 'Hello, Signal!';
const result = graphemeAndLinkAwareSlice(shortString, 50);
assert.strictEqual(result.text, shortString);
assert.isFalse(result.hasReadMore);
});
it('should return string longer than max but within buffer', () => {
const input = 'Hello, Signal!';
const result = graphemeAndLinkAwareSlice(input, 5, 10);
assert.strictEqual(result.text, input);
assert.isFalse(result.hasReadMore);
});
it('should include entire url and detect no more to read', () => {
const input = 'Hello, Signal! https://signal.org';
const result = graphemeAndLinkAwareSlice(input, 16, 0);
assert.strictEqual(result.text, input);
assert.isFalse(result.hasReadMore);
});
it('should include entire url and detect more to read', () => {
const input = 'Hello, Signal! https://signal.org additional text';
const inputProperlyTruncated = 'Hello, Signal! https://signal.org';
const result = graphemeAndLinkAwareSlice(input, 16, 0);
assert.strictEqual(result.text, inputProperlyTruncated);
assert.isTrue(result.hasReadMore);
});
it('should truncate normally when url present after truncation', () => {
const input = 'Hello, Signal! https://signal.org additional text';
const inputProperlyTruncated = 'Hello, Signal!';
const result = graphemeAndLinkAwareSlice(input, 14, 0);
assert.strictEqual(result.text, inputProperlyTruncated);
assert.isTrue(result.hasReadMore);
});
it('truncates after url when url present before and at truncation point', () => {
const input =
'Hello, Signal! https://signal.org additional text https://example.com/example more text';
const inputProperlyTruncated =
'Hello, Signal! https://signal.org additional text https://example.com/example';
const result = graphemeAndLinkAwareSlice(input, 55, 0);
assert.strictEqual(result.text, inputProperlyTruncated);
assert.isTrue(result.hasReadMore);
});
it('truncates after url when url present at and after truncation point', () => {
const input =
'Hello, Signal! https://signal.org additional text https://example.com/example more text';
const inputProperlyTruncated = 'Hello, Signal! https://signal.org';
const result = graphemeAndLinkAwareSlice(input, 26, 0);
assert.strictEqual(result.text, inputProperlyTruncated);
assert.isTrue(result.hasReadMore);
});
});

View file

@ -0,0 +1,68 @@
// Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import LinkifyIt from 'linkify-it';
export function graphemeAndLinkAwareSlice(
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) {
text = expandToIncludeEntireLink(str, text);
return {
text,
hasReadMore: text.length < str.length,
};
}
}
return {
text: str,
hasReadMore: false,
};
}
const expandToIncludeEntireLink = (
original: string,
truncated: string
): string => {
const linksInText = new LinkifyIt().match(original);
if (!linksInText) {
return truncated;
}
const invalidTruncationRanges: Array<LinkRange> = linksInText.map(
({ index: startIndex, lastIndex }) => ({ startIndex, lastIndex })
);
const truncatedLink: Array<LinkRange> = invalidTruncationRanges.filter(
({ startIndex, lastIndex }) =>
startIndex < truncated.length && lastIndex > truncated.length
);
if (truncatedLink.length === 0) return truncated;
return original.slice(0, truncatedLink[0].lastIndex);
};
type LinkRange = {
startIndex: number;
lastIndex: number;
};

View file

@ -1,34 +0,0 @@
// 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,
};
}