do not spit url between Read More chunks by expanding chunk to end of url
This commit is contained in:
parent
6ca3452488
commit
5df5cde48c
5 changed files with 142 additions and 38 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
70
ts/test-both/util/graphemeAndLinkAwareSlice.ts
Normal file
70
ts/test-both/util/graphemeAndLinkAwareSlice.ts
Normal 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);
|
||||
});
|
||||
});
|
68
ts/util/graphemeAndLinkAwareSlice.ts
Normal file
68
ts/util/graphemeAndLinkAwareSlice.ts
Normal 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;
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue