Affordances for really tall messages
This commit is contained in:
parent
2e9eaa855a
commit
b32d068e83
12 changed files with 375 additions and 108 deletions
|
@ -2711,6 +2711,10 @@
|
||||||
"message": "Cancel",
|
"message": "Cancel",
|
||||||
"description": "Appears on the cancel button in confirmation dialogs."
|
"description": "Appears on the cancel button in confirmation dialogs."
|
||||||
},
|
},
|
||||||
|
"MessageBody--read-more": {
|
||||||
|
"message": "Read more",
|
||||||
|
"description": "When a message is too long this is the affordance to expand the message"
|
||||||
|
},
|
||||||
"Message--unsupported-message": {
|
"Message--unsupported-message": {
|
||||||
"message": "$contact$ sent you a message that can't be processed or displayed because it uses a new Signal feature.",
|
"message": "$contact$ sent you a message that can't be processed or displayed because it uses a new Signal feature.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
|
@ -4518,48 +4518,6 @@ button.module-image__border-overlay:focus {
|
||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Module: Highlighted Message Body
|
|
||||||
|
|
||||||
.module-message-body__highlight {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-message-body__at-mention {
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block;
|
|
||||||
padding-left: 4px;
|
|
||||||
padding-right: 4px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
|
|
||||||
@include light-theme {
|
|
||||||
background-color: $color-gray-20;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include dark-theme {
|
|
||||||
background-color: $color-black-alpha-40;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border: 1px solid $color-black;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-message-body__at-mention--incoming {
|
|
||||||
@include light-theme {
|
|
||||||
background-color: $color-gray-20;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include dark-theme {
|
|
||||||
background-color: $color-gray-60;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-message-body__at-mention--outgoing {
|
|
||||||
background-color: $color-black-alpha-40;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Module: Reaction Viewer
|
// Module: Reaction Viewer
|
||||||
|
|
||||||
.module-reaction-viewer {
|
.module-reaction-viewer {
|
||||||
|
|
53
stylesheets/components/MessageBody.scss
Normal file
53
stylesheets/components/MessageBody.scss
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
.MessageBody {
|
||||||
|
&__highlight {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__read-more {
|
||||||
|
@include button-reset;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
color: $color-ultramarine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__at-mention {
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
background-color: $color-gray-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
background-color: $color-black-alpha-40;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: 1px solid $color-black;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--incoming {
|
||||||
|
@include light-theme {
|
||||||
|
background-color: $color-gray-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
background-color: $color-gray-60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--outgoing {
|
||||||
|
background-color: $color-black-alpha-40;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -68,6 +68,7 @@
|
||||||
@import './components/Lightbox.scss';
|
@import './components/Lightbox.scss';
|
||||||
@import './components/MediaQualitySelector.scss';
|
@import './components/MediaQualitySelector.scss';
|
||||||
@import './components/MessageAudio.scss';
|
@import './components/MessageAudio.scss';
|
||||||
|
@import './components/MessageBody.scss';
|
||||||
@import './components/MessageDetail.scss';
|
@import './components/MessageDetail.scss';
|
||||||
@import './components/Modal.scss';
|
@import './components/Modal.scss';
|
||||||
@import './components/PermissionsPopup.scss';
|
@import './components/PermissionsPopup.scss';
|
||||||
|
|
|
@ -46,7 +46,7 @@ export const AtMentionify = ({
|
||||||
if (range) {
|
if (range) {
|
||||||
results.push(
|
results.push(
|
||||||
<span
|
<span
|
||||||
className={`module-message-body__at-mention module-message-body__at-mention--${direction}`}
|
className={`MessageBody__at-mention MessageBody__at-mention--${direction}`}
|
||||||
key={range.start}
|
key={range.start}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (openConversation && range.conversationID) {
|
if (openConversation && range.conversationID) {
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||||
import { Avatar } from '../Avatar';
|
import { Avatar } from '../Avatar';
|
||||||
import { Spinner } from '../Spinner';
|
import { Spinner } from '../Spinner';
|
||||||
import { MessageBody } from './MessageBody';
|
import { MessageBodyReadMore } from './MessageBodyReadMore';
|
||||||
import { MessageMetadata } from './MessageMetadata';
|
import { MessageMetadata } from './MessageMetadata';
|
||||||
import { ImageGrid } from './ImageGrid';
|
import { ImageGrid } from './ImageGrid';
|
||||||
import { GIF } from './GIF';
|
import { GIF } from './GIF';
|
||||||
|
@ -1224,6 +1224,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
deletedForEveryone,
|
deletedForEveryone,
|
||||||
direction,
|
direction,
|
||||||
i18n,
|
i18n,
|
||||||
|
onHeightChange,
|
||||||
openConversation,
|
openConversation,
|
||||||
status,
|
status,
|
||||||
text,
|
text,
|
||||||
|
@ -1252,12 +1253,13 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
: null
|
: null
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<MessageBody
|
<MessageBodyReadMore
|
||||||
bodyRanges={bodyRanges}
|
bodyRanges={bodyRanges}
|
||||||
disableLinks={!this.areLinksEnabled()}
|
disableLinks={!this.areLinksEnabled()}
|
||||||
direction={direction}
|
direction={direction}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
openConversation={openConversation}
|
openConversation={openConversation}
|
||||||
|
onHeightChange={onHeightChange}
|
||||||
text={contents || ''}
|
text={contents || ''}
|
||||||
textPending={textPending}
|
textPending={textPending}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
// Copyright 2018-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React, { KeyboardEvent } from 'react';
|
||||||
|
|
||||||
import { getSizeClass, SizeClassType } from '../emoji/lib';
|
import { getSizeClass, SizeClassType } from '../emoji/lib';
|
||||||
import { AtMentionify } from './AtMentionify';
|
import { AtMentionify } from './AtMentionify';
|
||||||
|
@ -30,6 +30,7 @@ export type Props = {
|
||||||
disableLinks?: boolean;
|
disableLinks?: boolean;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
bodyRanges?: BodyRangesType;
|
bodyRanges?: BodyRangesType;
|
||||||
|
onIncreaseTextLength?: () => unknown;
|
||||||
openConversation?: OpenConversationActionType;
|
openConversation?: OpenConversationActionType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -59,21 +60,39 @@ const renderEmoji = ({
|
||||||
* configurable with their `renderXXX` props, this component will assemble all three of
|
* configurable with their `renderXXX` props, this component will assemble all three of
|
||||||
* them for you.
|
* them for you.
|
||||||
*/
|
*/
|
||||||
export class MessageBody extends React.Component<Props> {
|
export function MessageBody({
|
||||||
private readonly renderNewLines: RenderTextCallbackType = ({
|
bodyRanges,
|
||||||
|
direction,
|
||||||
|
disableJumbomoji,
|
||||||
|
disableLinks,
|
||||||
|
i18n,
|
||||||
|
onIncreaseTextLength,
|
||||||
|
openConversation,
|
||||||
|
text,
|
||||||
|
textPending,
|
||||||
|
}: Props): JSX.Element {
|
||||||
|
const hasReadMore = Boolean(onIncreaseTextLength);
|
||||||
|
const textWithSuffix = textPending || hasReadMore ? `${text}...` : text;
|
||||||
|
|
||||||
|
const sizeClass = disableJumbomoji ? undefined : getSizeClass(text);
|
||||||
|
const processedText = AtMentionify.preprocessMentions(
|
||||||
|
textWithSuffix,
|
||||||
|
bodyRanges
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderNewLines: RenderTextCallbackType = ({
|
||||||
text: textWithNewLines,
|
text: textWithNewLines,
|
||||||
key,
|
key,
|
||||||
}) => {
|
}) => {
|
||||||
const { bodyRanges, direction, openConversation } = this.props;
|
|
||||||
return (
|
return (
|
||||||
<AddNewLines
|
<AddNewLines
|
||||||
key={key}
|
key={key}
|
||||||
text={textWithNewLines}
|
text={textWithNewLines}
|
||||||
renderNonNewLine={({ text, key: innerKey }) => (
|
renderNonNewLine={({ text: innerText, key: innerKey }) => (
|
||||||
<AtMentionify
|
<AtMentionify
|
||||||
key={innerKey}
|
key={innerKey}
|
||||||
direction={direction}
|
direction={direction}
|
||||||
text={text}
|
text={innerText}
|
||||||
bodyRanges={bodyRanges}
|
bodyRanges={bodyRanges}
|
||||||
openConversation={openConversation}
|
openConversation={openConversation}
|
||||||
/>
|
/>
|
||||||
|
@ -82,62 +101,51 @@ export class MessageBody extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public addDownloading(jsx: JSX.Element): JSX.Element {
|
return (
|
||||||
const { i18n, textPending } = this.props;
|
<span>
|
||||||
|
{disableLinks ? (
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{jsx}
|
|
||||||
{textPending ? (
|
|
||||||
<span className="module-message-body__highlight">
|
|
||||||
{' '}
|
|
||||||
{i18n('downloading')}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
|
||||||
const {
|
|
||||||
bodyRanges,
|
|
||||||
text,
|
|
||||||
textPending,
|
|
||||||
disableJumbomoji,
|
|
||||||
disableLinks,
|
|
||||||
i18n,
|
|
||||||
} = this.props;
|
|
||||||
const sizeClass = disableJumbomoji ? undefined : getSizeClass(text);
|
|
||||||
const textWithPending = AtMentionify.preprocessMentions(
|
|
||||||
textPending ? `${text}...` : text,
|
|
||||||
bodyRanges
|
|
||||||
);
|
|
||||||
|
|
||||||
if (disableLinks) {
|
|
||||||
return this.addDownloading(
|
|
||||||
renderEmoji({
|
renderEmoji({
|
||||||
i18n,
|
i18n,
|
||||||
text: textWithPending,
|
text: processedText,
|
||||||
sizeClass,
|
sizeClass,
|
||||||
key: 0,
|
key: 0,
|
||||||
renderNonEmoji: this.renderNewLines,
|
renderNonEmoji: renderNewLines,
|
||||||
})
|
})
|
||||||
);
|
) : (
|
||||||
}
|
<Linkify
|
||||||
|
text={processedText}
|
||||||
return this.addDownloading(
|
renderNonLink={({ key, text: nonLinkText }) => {
|
||||||
<Linkify
|
return renderEmoji({
|
||||||
text={textWithPending}
|
i18n,
|
||||||
renderNonLink={({ key, text: nonLinkText }) => {
|
text: nonLinkText,
|
||||||
return renderEmoji({
|
sizeClass,
|
||||||
i18n,
|
key,
|
||||||
text: nonLinkText,
|
renderNonEmoji: renderNewLines,
|
||||||
sizeClass,
|
});
|
||||||
key,
|
}}
|
||||||
renderNonEmoji: this.renderNewLines,
|
/>
|
||||||
});
|
)}
|
||||||
}}
|
{textPending ? (
|
||||||
/>
|
<span className="MessageBody__highlight"> {i18n('downloading')}</span>
|
||||||
);
|
) : null}
|
||||||
}
|
{onIncreaseTextLength ? (
|
||||||
|
<button
|
||||||
|
className="MessageBody__read-more"
|
||||||
|
onClick={() => {
|
||||||
|
onIncreaseTextLength();
|
||||||
|
}}
|
||||||
|
onKeyDown={(ev: KeyboardEvent) => {
|
||||||
|
if (ev.key === 'Space' || ev.key === 'Enter') {
|
||||||
|
onIncreaseTextLength();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
{i18n('MessageBody--read-more')}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
153
ts/components/conversation/MessageBodyReadMore.stories.tsx
Normal file
153
ts/components/conversation/MessageBodyReadMore.stories.tsx
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { text } from '@storybook/addon-knobs';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
|
import { MessageBodyReadMore, Props } from './MessageBodyReadMore';
|
||||||
|
import { setupI18n } from '../../util/setupI18n';
|
||||||
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
const story = storiesOf('Components/Conversation/MessageBodyReadMore', module);
|
||||||
|
|
||||||
|
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
|
bodyRanges: overrideProps.bodyRanges,
|
||||||
|
direction: 'incoming',
|
||||||
|
i18n,
|
||||||
|
onHeightChange: action('onHeightChange'),
|
||||||
|
text: text('text', overrideProps.text || ''),
|
||||||
|
});
|
||||||
|
|
||||||
|
story.add('Lots of cake with a cherry on top', () => (
|
||||||
|
<MessageBodyReadMore
|
||||||
|
{...createProps({
|
||||||
|
text: `x${'🍰'.repeat(399)}🍒`,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
story.add('Cherry overflow', () => (
|
||||||
|
<MessageBodyReadMore
|
||||||
|
{...createProps({
|
||||||
|
text: `x${'🍰'.repeat(400)}🍒`,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
story.add('Excessive amounts of cake', () => (
|
||||||
|
<MessageBodyReadMore
|
||||||
|
{...createProps({
|
||||||
|
text: `x${'🍰'.repeat(20000)}`,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
story.add('Long text', () => (
|
||||||
|
<MessageBodyReadMore
|
||||||
|
{...createProps({
|
||||||
|
text: `
|
||||||
|
SCENE I. Rome. A street.
|
||||||
|
Enter FLAVIUS, MARULLUS, and certain Commoners
|
||||||
|
FLAVIUS
|
||||||
|
Hence! home, you idle creatures get you home:
|
||||||
|
Is this a holiday? what! know you not,
|
||||||
|
Being mechanical, you ought not walk
|
||||||
|
Upon a labouring day without the sign
|
||||||
|
Of your profession? Speak, what trade art thou?
|
||||||
|
First Commoner
|
||||||
|
Why, sir, a carpenter.
|
||||||
|
MARULLUS
|
||||||
|
Where is thy leather apron and thy rule?
|
||||||
|
What dost thou with thy best apparel on?
|
||||||
|
You, sir, what trade are you?
|
||||||
|
Second Commoner
|
||||||
|
Truly, sir, in respect of a fine workman, I am but,
|
||||||
|
as you would say, a cobbler.
|
||||||
|
MARULLUS
|
||||||
|
But what trade art thou? answer me directly.
|
||||||
|
Second Commoner
|
||||||
|
A trade, sir, that, I hope, I may use with a safe
|
||||||
|
conscience; which is, indeed, sir, a mender of bad soles.
|
||||||
|
MARULLUS
|
||||||
|
What trade, thou knave? thou naughty knave, what trade?
|
||||||
|
Second Commoner
|
||||||
|
Nay, I beseech you, sir, be not out with me: yet,
|
||||||
|
if you be out, sir, I can mend you.
|
||||||
|
MARULLUS
|
||||||
|
What meanest thou by that? mend me, thou saucy fellow!
|
||||||
|
Second Commoner
|
||||||
|
Why, sir, cobble you.
|
||||||
|
FLAVIUS
|
||||||
|
Thou art a cobbler, art thou?
|
||||||
|
Second Commoner
|
||||||
|
Truly, sir, all that I live by is with the awl: I
|
||||||
|
meddle with no tradesman's matters, nor women's
|
||||||
|
matters, but with awl. I am, indeed, sir, a surgeon
|
||||||
|
to old shoes; when they are in great danger, I
|
||||||
|
recover them. As proper men as ever trod upon
|
||||||
|
neat's leather have gone upon my handiwork.
|
||||||
|
FLAVIUS
|
||||||
|
But wherefore art not in thy shop today?
|
||||||
|
Why dost thou lead these men about the streets?
|
||||||
|
Second Commoner
|
||||||
|
Truly, sir, to wear out their shoes, to get myself
|
||||||
|
into more work. But, indeed, sir, we make holiday,
|
||||||
|
to see Caesar and to rejoice in his triumph.
|
||||||
|
MARULLUS
|
||||||
|
Wherefore rejoice? What conquest brings he home?
|
||||||
|
What tributaries follow him to Rome,
|
||||||
|
To grace in captive bonds his chariot-wheels?
|
||||||
|
You blocks, you stones, you worse than senseless things!
|
||||||
|
O you hard hearts, you cruel men of Rome,
|
||||||
|
Knew you not Pompey? Many a time and oft
|
||||||
|
Have you climb'd up to walls and battlements,
|
||||||
|
To towers and windows, yea, to chimney-tops,
|
||||||
|
Your infants in your arms, and there have sat
|
||||||
|
The livelong day, with patient expectation,
|
||||||
|
To see great Pompey pass the streets of Rome:
|
||||||
|
And when you saw his chariot but appear,
|
||||||
|
Have you not made an universal shout,
|
||||||
|
That Tiber trembled underneath her banks,
|
||||||
|
To hear the replication of your sounds
|
||||||
|
Made in her concave shores?
|
||||||
|
And do you now put on your best attire?
|
||||||
|
And do you now cull out a holiday?
|
||||||
|
And do you now strew flowers in his way
|
||||||
|
That comes in triumph over Pompey's blood? Be gone!
|
||||||
|
Run to your houses, fall upon your knees,
|
||||||
|
Pray to the gods to intermit the plague
|
||||||
|
That needs must light on this ingratitude.
|
||||||
|
FLAVIUS
|
||||||
|
Go, go, good countrymen, and, for this fault,
|
||||||
|
Assemble all the poor men of your sort;
|
||||||
|
Draw them to Tiber banks, and weep your tears
|
||||||
|
Into the channel, till the lowest stream
|
||||||
|
Do kiss the most exalted shores of all.
|
||||||
|
Exeunt all the Commoners
|
||||||
|
See whether their basest metal be not moved;
|
||||||
|
They vanish tongue-tied in their guiltiness.
|
||||||
|
Go you down that way towards the Capitol;
|
||||||
|
This way will I
|
||||||
|
disrobe the images,
|
||||||
|
If you do find them deck'd with ceremonies.
|
||||||
|
MARULLUS
|
||||||
|
May we do so?
|
||||||
|
You know it is the feast of Lupercal.
|
||||||
|
FLAVIUS
|
||||||
|
It is no matter; let no images
|
||||||
|
Be hung with Caesar's trophies. I'll about,
|
||||||
|
And drive away the vulgar from the streets:
|
||||||
|
So do you too, where you perceive them thick.
|
||||||
|
These growing feathers pluck'd from Caesar's wing
|
||||||
|
Will make him fly an ordinary pitch,
|
||||||
|
Who else would soar above the view of men
|
||||||
|
And keep us all in servile fearfulness.
|
||||||
|
`,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
));
|
88
ts/components/conversation/MessageBodyReadMore.tsx
Normal file
88
ts/components/conversation/MessageBodyReadMore.tsx
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { MessageBody, Props as MessageBodyPropsType } from './MessageBody';
|
||||||
|
|
||||||
|
export type Props = Pick<
|
||||||
|
MessageBodyPropsType,
|
||||||
|
| 'direction'
|
||||||
|
| 'text'
|
||||||
|
| 'textPending'
|
||||||
|
| 'disableLinks'
|
||||||
|
| 'i18n'
|
||||||
|
| 'bodyRanges'
|
||||||
|
| 'openConversation'
|
||||||
|
> & {
|
||||||
|
onHeightChange: () => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
const INITIAL_LENGTH = 800;
|
||||||
|
const INCREMENT_COUNT = 3000;
|
||||||
|
|
||||||
|
function graphemeAwareSlice(
|
||||||
|
str: string,
|
||||||
|
length: number
|
||||||
|
): {
|
||||||
|
hasReadMore: boolean;
|
||||||
|
text: string;
|
||||||
|
} {
|
||||||
|
if (str.length <= length) {
|
||||||
|
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,
|
||||||
|
disableLinks,
|
||||||
|
i18n,
|
||||||
|
onHeightChange,
|
||||||
|
openConversation,
|
||||||
|
text,
|
||||||
|
textPending,
|
||||||
|
}: Props): JSX.Element {
|
||||||
|
const [maxLength, setMaxLength] = useState(INITIAL_LENGTH);
|
||||||
|
|
||||||
|
const { hasReadMore, text: slicedText } = graphemeAwareSlice(text, maxLength);
|
||||||
|
|
||||||
|
const onIncreaseTextLength = hasReadMore
|
||||||
|
? () => {
|
||||||
|
setMaxLength(oldMaxLength => oldMaxLength + INCREMENT_COUNT);
|
||||||
|
onHeightChange();
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MessageBody
|
||||||
|
bodyRanges={bodyRanges}
|
||||||
|
disableLinks={disableLinks}
|
||||||
|
direction={direction}
|
||||||
|
i18n={i18n}
|
||||||
|
onIncreaseTextLength={onIncreaseTextLength}
|
||||||
|
openConversation={openConversation}
|
||||||
|
text={slicedText}
|
||||||
|
textPending={textPending}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -105,7 +105,7 @@ export class MessageBodyHighlight extends React.Component<Props> {
|
||||||
const [, toHighlight] = match;
|
const [, toHighlight] = match;
|
||||||
count += 2;
|
count += 2;
|
||||||
results.push(
|
results.push(
|
||||||
<span className="module-message-body__highlight" key={count - 1}>
|
<span className="MessageBody__highlight" key={count - 1}>
|
||||||
{renderEmoji({
|
{renderEmoji({
|
||||||
text: toHighlight,
|
text: toHighlight,
|
||||||
sizeClass,
|
sizeClass,
|
||||||
|
|
|
@ -13,7 +13,7 @@ export const matchMention = (
|
||||||
if (memberRepository) {
|
if (memberRepository) {
|
||||||
const { title } = node.dataset;
|
const { title } = node.dataset;
|
||||||
|
|
||||||
if (node.classList.contains('module-message-body__at-mention')) {
|
if (node.classList.contains('MessageBody__at-mention')) {
|
||||||
const { id } = node.dataset;
|
const { id } = node.dataset;
|
||||||
const conversation = memberRepository.getMemberById(id);
|
const conversation = memberRepository.getMemberById(id);
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ const createMockElement = (
|
||||||
|
|
||||||
const createMockAtMentionElement = (
|
const createMockAtMentionElement = (
|
||||||
dataset: Record<string, string>
|
dataset: Record<string, string>
|
||||||
): HTMLElement => createMockElement('module-message-body__at-mention', dataset);
|
): HTMLElement => createMockElement('MessageBody__at-mention', dataset);
|
||||||
|
|
||||||
const createMockMentionBlotElement = (
|
const createMockMentionBlotElement = (
|
||||||
dataset: Record<string, string>
|
dataset: Record<string, string>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue