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 { getAvatarColor } from '../types/Colors';
|
||||||
import { getStoryBackground } from '../util/getStoryBackground';
|
import { getStoryBackground } from '../util/getStoryBackground';
|
||||||
import { getStoryDuration } from '../util/getStoryDuration';
|
import { getStoryDuration } from '../util/getStoryDuration';
|
||||||
import { graphemeAwareSlice } from '../util/graphemeAwareSlice';
|
|
||||||
import type { saveAttachment } from '../util/saveAttachment';
|
import type { saveAttachment } from '../util/saveAttachment';
|
||||||
import { isVideoAttachment } from '../types/Attachment';
|
import { isVideoAttachment } from '../types/Attachment';
|
||||||
|
import { graphemeAndLinkAwareSlice } from '../util/graphemeAndLinkAwareSlice';
|
||||||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||||
import { useRetryStorySend } from '../hooks/useRetryStorySend';
|
import { useRetryStorySend } from '../hooks/useRetryStorySend';
|
||||||
import { resolveStorySendStatus } from '../util/resolveStorySendStatus';
|
import { resolveStorySendStatus } from '../util/resolveStorySendStatus';
|
||||||
|
@ -228,7 +228,7 @@ export function StoryViewer({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return graphemeAwareSlice(
|
return graphemeAndLinkAwareSlice(
|
||||||
attachment.caption,
|
attachment.caption,
|
||||||
hasExpandedCaption ? CAPTION_MAX_LENGTH : CAPTION_INITIAL_LENGTH,
|
hasExpandedCaption ? CAPTION_MAX_LENGTH : CAPTION_INITIAL_LENGTH,
|
||||||
CAPTION_BUFFER
|
CAPTION_BUFFER
|
||||||
|
|
|
@ -5,7 +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';
|
import { graphemeAndLinkAwareSlice } from '../../util/graphemeAndLinkAwareSlice';
|
||||||
|
|
||||||
export type Props = Pick<
|
export type Props = Pick<
|
||||||
MessageBodyPropsType,
|
MessageBodyPropsType,
|
||||||
|
@ -46,7 +46,7 @@ export function MessageBodyReadMore({
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
const maxLength = displayLimit || INITIAL_LENGTH;
|
const maxLength = displayLimit || INITIAL_LENGTH;
|
||||||
|
|
||||||
const { hasReadMore, text: slicedText } = graphemeAwareSlice(
|
const { hasReadMore, text: slicedText } = graphemeAndLinkAwareSlice(
|
||||||
text,
|
text,
|
||||||
maxLength,
|
maxLength,
|
||||||
BUFFER
|
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…
Add table
Add a link
Reference in a new issue