@mentions receive support
This commit is contained in:
parent
c126a71864
commit
9657c38987
18 changed files with 555 additions and 23 deletions
|
@ -1552,6 +1552,7 @@
|
|||
return {
|
||||
author: contact.get('e164'),
|
||||
authorUuid: contact.get('uuid'),
|
||||
bodyRanges: quotedMessage.get('bodyRanges'),
|
||||
id: quotedMessage.get('sent_at'),
|
||||
text: body || embeddedContactName,
|
||||
attachments: quotedMessage.isTapToView()
|
||||
|
|
|
@ -660,9 +660,49 @@
|
|||
isTapToView && this.isIncoming() && this.get('isTapToViewInvalid'),
|
||||
|
||||
deletedForEveryone: this.get('deletedForEveryone') || false,
|
||||
bodyRanges: this.processBodyRanges(),
|
||||
};
|
||||
},
|
||||
|
||||
processBodyRanges(bodyRanges = this.get('bodyRanges')) {
|
||||
if (!bodyRanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return (
|
||||
bodyRanges
|
||||
.map(range => {
|
||||
if (range.mentionUuid) {
|
||||
const contactID = ConversationController.ensureContactIds({
|
||||
uuid: range.mentionUuid,
|
||||
});
|
||||
const conversation = this.findContact(contactID);
|
||||
|
||||
return {
|
||||
...range,
|
||||
conversationID: contactID,
|
||||
replacementText: conversation.getTitle(),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
// sorting in a descending order so that we can safely replace the
|
||||
// positions in the text
|
||||
.sort((a, b) => b.start - a.start)
|
||||
);
|
||||
},
|
||||
|
||||
getTextWithMentionStrings(bodyRanges, text) {
|
||||
return bodyRanges.reduce((str, range) => {
|
||||
const textBegin = str.substr(0, range.start);
|
||||
const textEnd = str.substr(range.start + range.length, str.length);
|
||||
return `${textBegin}@${range.replacementText}${textEnd}`;
|
||||
}, text);
|
||||
},
|
||||
|
||||
// Dependencies of prop-generation functions
|
||||
findAndFormatContact(identifier) {
|
||||
if (!identifier) {
|
||||
|
@ -822,9 +862,12 @@
|
|||
const {
|
||||
author,
|
||||
authorUuid,
|
||||
bodyRanges,
|
||||
id: sentAt,
|
||||
referencedMessageNotFound,
|
||||
text,
|
||||
} = quote;
|
||||
|
||||
const contact =
|
||||
(author || authorUuid) &&
|
||||
ConversationController.get(
|
||||
|
@ -845,10 +888,11 @@
|
|||
const firstAttachment = quote.attachments && quote.attachments[0];
|
||||
|
||||
return {
|
||||
text: this.createNonBreakingLastSeparator(quote.text),
|
||||
text: this.createNonBreakingLastSeparator(text),
|
||||
attachment: firstAttachment
|
||||
? this.processQuoteAttachment(firstAttachment)
|
||||
: null,
|
||||
bodyRanges: this.processBodyRanges(bodyRanges),
|
||||
isFromMe,
|
||||
sentAt,
|
||||
authorId: author,
|
||||
|
@ -1154,16 +1198,25 @@
|
|||
getNotificationText() /* : string */ {
|
||||
const { text, emoji } = this.getNotificationData();
|
||||
|
||||
let modifiedText = text;
|
||||
|
||||
const hasMentions = Boolean(this.get('bodyRanges'));
|
||||
|
||||
if (hasMentions) {
|
||||
const bodyRanges = this.processBodyRanges();
|
||||
modifiedText = this.getTextWithMentionStrings(bodyRanges, modifiedText);
|
||||
}
|
||||
|
||||
// Linux emoji support is mixed, so we disable it. (Note that this doesn't touch
|
||||
// the `text`, which can contain emoji.)
|
||||
const shouldIncludeEmoji = Boolean(emoji) && !Signal.OS.isLinux();
|
||||
if (shouldIncludeEmoji) {
|
||||
return i18n('message--getNotificationText--text-with-emoji', {
|
||||
text,
|
||||
text: modifiedText,
|
||||
emoji,
|
||||
});
|
||||
}
|
||||
return text;
|
||||
return modifiedText;
|
||||
},
|
||||
|
||||
// General
|
||||
|
@ -2567,6 +2620,7 @@
|
|||
id: window.getGuid(),
|
||||
attachments: dataMessage.attachments,
|
||||
body: dataMessage.body,
|
||||
bodyRanges: dataMessage.bodyRanges,
|
||||
contact: dataMessage.contact,
|
||||
conversationId: conversation.id,
|
||||
decrypted_at: now,
|
||||
|
|
|
@ -116,6 +116,7 @@ message DataMessage {
|
|||
optional string authorUuid = 5;
|
||||
optional string text = 3;
|
||||
repeated QuotedAttachment attachments = 4;
|
||||
repeated BodyRange bodyRanges = 6;
|
||||
}
|
||||
|
||||
message Contact {
|
||||
|
@ -212,6 +213,15 @@ message DataMessage {
|
|||
optional uint64 targetSentTimestamp = 1;
|
||||
}
|
||||
|
||||
message BodyRange {
|
||||
optional uint32 start = 1;
|
||||
optional uint32 length = 2;
|
||||
|
||||
// oneof associatedValue {
|
||||
optional string mentionUuid = 3;
|
||||
//}
|
||||
}
|
||||
|
||||
enum ProtocolVersion {
|
||||
option allow_alias = true;
|
||||
|
||||
|
@ -221,7 +231,8 @@ message DataMessage {
|
|||
VIEW_ONCE_VIDEO = 3;
|
||||
REACTIONS = 4;
|
||||
CDN_SELECTOR_ATTACHMENTS = 5;
|
||||
CURRENT = 5;
|
||||
MENTIONS = 6;
|
||||
CURRENT = 6;
|
||||
}
|
||||
|
||||
optional string body = 1;
|
||||
|
@ -240,6 +251,7 @@ message DataMessage {
|
|||
optional bool isViewOnce = 14;
|
||||
optional Reaction reaction = 16;
|
||||
optional Delete delete = 17;
|
||||
repeated BodyRange bodyRanges = 18;
|
||||
}
|
||||
|
||||
message NullMessage {
|
||||
|
|
|
@ -5265,6 +5265,46 @@ button.module-image__border-overlay:focus {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.module-message-body__at-mention {
|
||||
background-color: $color-black-alpha-40;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
|
||||
&:focus {
|
||||
border: solid 1px $color-black;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.module-message-body__at-mention--incoming {
|
||||
@include ios-theme {
|
||||
@include light-theme {
|
||||
background-color: $color-gray-20;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
background-color: $color-gray-60;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-message-body__at-mention--outgoing {
|
||||
@include light-theme {
|
||||
background-color: $color-gray-20;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
background-color: $color-gray-60;
|
||||
}
|
||||
|
||||
@include ios-theme {
|
||||
background-color: $ultramarine-brand-dark;
|
||||
}
|
||||
}
|
||||
|
||||
// Module: Search Results
|
||||
|
||||
.module-search-results {
|
||||
|
|
|
@ -13,6 +13,7 @@ $color-white: #ffffff;
|
|||
$color-gray-02: #f6f6f6;
|
||||
$color-gray-05: #e9e9e9;
|
||||
$color-gray-15: #dedede;
|
||||
$color-gray-20: #c6c6c6;
|
||||
$color-gray-25: #b9b9b9;
|
||||
$color-gray-45: #848484;
|
||||
$color-gray-60: #5e5e5e;
|
||||
|
|
|
@ -253,3 +253,16 @@ story.add('Muted Conversation', () => {
|
|||
|
||||
return <ConversationListItem {...props} muteExpiresAt={muteExpiresAt} />;
|
||||
});
|
||||
|
||||
story.add('At Mention', () => {
|
||||
const props = createProps({
|
||||
title: 'The Rebellion',
|
||||
type: 'group',
|
||||
lastMessage: {
|
||||
text: '@Leia Organa I know',
|
||||
status: 'read',
|
||||
},
|
||||
});
|
||||
|
||||
return <ConversationListItem {...props} />;
|
||||
});
|
||||
|
|
91
ts/components/conversation/AtMentionify.stories.tsx
Normal file
91
ts/components/conversation/AtMentionify.stories.tsx
Normal file
|
@ -0,0 +1,91 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { select, text } from '@storybook/addon-knobs';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
|
||||
import { AtMentionify, Props } from './AtMentionify';
|
||||
|
||||
const story = storiesOf('Components/Conversation/AtMentionify', module);
|
||||
|
||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
bodyRanges: overrideProps.bodyRanges,
|
||||
direction: select(
|
||||
'direction',
|
||||
{ incoming: 'incoming', outgoing: 'outgoing' },
|
||||
overrideProps.direction || 'incoming'
|
||||
),
|
||||
openConversation: action('openConversation'),
|
||||
text: text('text', overrideProps.text || ''),
|
||||
});
|
||||
|
||||
story.add('No @mentions', () => {
|
||||
const props = createProps({
|
||||
text: 'Hello World',
|
||||
});
|
||||
|
||||
return <AtMentionify {...props} />;
|
||||
});
|
||||
|
||||
story.add('Multiple @Mentions', () => {
|
||||
const bodyRanges = [
|
||||
{
|
||||
start: 4,
|
||||
length: 1,
|
||||
mentionUuid: 'abc',
|
||||
replacementText: 'Professor Farnsworth',
|
||||
},
|
||||
{
|
||||
start: 2,
|
||||
length: 1,
|
||||
mentionUuid: 'def',
|
||||
replacementText: 'Philip J Fry',
|
||||
},
|
||||
{
|
||||
start: 0,
|
||||
length: 1,
|
||||
mentionUuid: 'xyz',
|
||||
replacementText: 'Yancy Fry',
|
||||
},
|
||||
];
|
||||
const props = createProps({
|
||||
bodyRanges,
|
||||
direction: 'outgoing',
|
||||
text: AtMentionify.preprocessMentions('\uFFFC \uFFFC \uFFFC', bodyRanges),
|
||||
});
|
||||
|
||||
return <AtMentionify {...props} />;
|
||||
});
|
||||
|
||||
story.add('Complex @mentions', () => {
|
||||
const bodyRanges = [
|
||||
{
|
||||
start: 80,
|
||||
length: 1,
|
||||
mentionUuid: 'ioe',
|
||||
replacementText: 'Cereal Killer',
|
||||
},
|
||||
{
|
||||
start: 78,
|
||||
length: 1,
|
||||
mentionUuid: 'fdr',
|
||||
replacementText: 'Acid Burn',
|
||||
},
|
||||
{
|
||||
start: 4,
|
||||
length: 1,
|
||||
mentionUuid: 'ope',
|
||||
replacementText: 'Zero Cool',
|
||||
},
|
||||
];
|
||||
|
||||
const props = createProps({
|
||||
bodyRanges,
|
||||
text: AtMentionify.preprocessMentions(
|
||||
'Hey \uFFFC\nCheck out https://www.signal.org I think you will really like it 😍\n\ncc \uFFFC \uFFFC',
|
||||
bodyRanges
|
||||
),
|
||||
});
|
||||
|
||||
return <AtMentionify {...props} />;
|
||||
});
|
104
ts/components/conversation/AtMentionify.tsx
Normal file
104
ts/components/conversation/AtMentionify.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
import React from 'react';
|
||||
import { Emojify } from './Emojify';
|
||||
import { BodyRangesType } from '../../types/Util';
|
||||
|
||||
export type Props = {
|
||||
bodyRanges?: BodyRangesType;
|
||||
direction?: 'incoming' | 'outgoing';
|
||||
openConversation?: (conversationId: string, messageId?: string) => void;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export const AtMentionify = ({
|
||||
bodyRanges,
|
||||
direction,
|
||||
openConversation,
|
||||
text,
|
||||
}: Props): JSX.Element => {
|
||||
if (!bodyRanges) {
|
||||
return <>{text}</>;
|
||||
}
|
||||
|
||||
const MENTIONS_REGEX = /(\uFFFC@(\d+))/g;
|
||||
|
||||
let match = MENTIONS_REGEX.exec(text);
|
||||
let last = 0;
|
||||
|
||||
const rangeStarts = new Map();
|
||||
bodyRanges.forEach(range => {
|
||||
rangeStarts.set(range.start, range);
|
||||
});
|
||||
|
||||
const results = [];
|
||||
while (match) {
|
||||
if (last < match.index) {
|
||||
const textWithNoMentions = text.slice(last, match.index);
|
||||
results.push(textWithNoMentions);
|
||||
}
|
||||
|
||||
const rangeStart = Number(match[2]);
|
||||
const range = rangeStarts.get(rangeStart);
|
||||
|
||||
if (range) {
|
||||
results.push(
|
||||
<span
|
||||
className={`module-message-body__at-mention module-message-body__at-mention--${direction}`}
|
||||
key={range.start}
|
||||
onClick={() => {
|
||||
if (openConversation && range.conversationID) {
|
||||
openConversation(range.conversationID);
|
||||
}
|
||||
}}
|
||||
onKeyUp={e => {
|
||||
if (
|
||||
e.target === e.currentTarget &&
|
||||
e.keyCode === 13 &&
|
||||
openConversation &&
|
||||
range.conversationID
|
||||
) {
|
||||
openConversation(range.conversationID);
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="link"
|
||||
>
|
||||
@
|
||||
<Emojify text={range.replacementText} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
last = MENTIONS_REGEX.lastIndex;
|
||||
match = MENTIONS_REGEX.exec(text);
|
||||
}
|
||||
|
||||
if (last < text.length) {
|
||||
results.push(text.slice(last));
|
||||
}
|
||||
|
||||
return <>{results}</>;
|
||||
};
|
||||
|
||||
// At-mentions need to be pre-processed before being pushed through the
|
||||
// AtMentionify component, this is due to bodyRanges containing start+length
|
||||
// values that operate on the raw string. The text has to be passed through
|
||||
// other components before being rendered in the <MessageBody />, components
|
||||
// such as Linkify, and Emojify. These components receive the text prop as a
|
||||
// string, therefore we're unable to mark it up with DOM nodes prior to handing
|
||||
// it off to them. This function will encode the "start" position into the text
|
||||
// string so we can later pull it off when rendering the @mention.
|
||||
AtMentionify.preprocessMentions = (
|
||||
text: string,
|
||||
bodyRanges?: BodyRangesType
|
||||
): string => {
|
||||
if (!bodyRanges || !bodyRanges.length) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return bodyRanges.reduce((str, range) => {
|
||||
const textBegin = str.substr(0, range.start);
|
||||
const encodedMention = `\uFFFC@${range.start}`;
|
||||
const textEnd = str.substr(range.start + range.length, str.length);
|
||||
return `${textBegin}${encodedMention}${textEnd}`;
|
||||
}, text);
|
||||
};
|
|
@ -45,6 +45,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
authorColor: overrideProps.authorColor || 'blue',
|
||||
authorAvatarPath: overrideProps.authorAvatarPath,
|
||||
authorTitle: text('authorTitle', overrideProps.authorTitle || ''),
|
||||
bodyRanges: overrideProps.bodyRanges,
|
||||
canReply: true,
|
||||
clearSelectedMessage: action('clearSelectedMessage'),
|
||||
collapseMetadata: overrideProps.collapseMetadata,
|
||||
|
@ -769,3 +770,19 @@ story.add('Colors', () => {
|
|||
</>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('@Mentions', () => {
|
||||
const props = createProps({
|
||||
bodyRanges: [
|
||||
{
|
||||
start: 0,
|
||||
length: 1,
|
||||
mentionUuid: 'zap',
|
||||
replacementText: 'Zapp Brannigan',
|
||||
},
|
||||
],
|
||||
text: '\uFFFC This Is It. The Moment We Should Have Trained For.',
|
||||
});
|
||||
|
||||
return renderBothDirections(props);
|
||||
});
|
||||
|
|
|
@ -40,7 +40,7 @@ import { ContactType } from '../../types/Contact';
|
|||
|
||||
import { getIncrement } from '../../util/timer';
|
||||
import { isFileDangerous } from '../../util/isFileDangerous';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { BodyRangesType, LocalizerType } from '../../types/Util';
|
||||
import { ColorType } from '../../types/Colors';
|
||||
import { createRefMerger } from '../_util';
|
||||
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
|
||||
|
@ -116,6 +116,7 @@ export type PropsData = {
|
|||
authorTitle: string;
|
||||
authorName?: string;
|
||||
authorColor?: ColorType;
|
||||
bodyRanges?: BodyRangesType;
|
||||
referencedMessageNotFound: boolean;
|
||||
};
|
||||
previews: Array<LinkPreviewType>;
|
||||
|
@ -135,6 +136,7 @@ export type PropsData = {
|
|||
deletedForEveryone?: boolean;
|
||||
|
||||
canReply: boolean;
|
||||
bodyRanges?: BodyRangesType;
|
||||
};
|
||||
|
||||
export type PropsHousekeeping = {
|
||||
|
@ -905,6 +907,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
direction,
|
||||
disableScroll,
|
||||
i18n,
|
||||
openConversation,
|
||||
quote,
|
||||
scrollToQuotedMessage,
|
||||
} = this.props;
|
||||
|
@ -940,6 +943,8 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
authorName={quote.authorName}
|
||||
authorColor={quoteColor}
|
||||
authorTitle={quote.authorTitle}
|
||||
bodyRanges={quote.bodyRanges}
|
||||
openConversation={openConversation}
|
||||
referencedMessageNotFound={referencedMessageNotFound}
|
||||
isFromMe={quote.isFromMe}
|
||||
withContentAbove={withContentAbove}
|
||||
|
@ -1045,9 +1050,11 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
|
||||
public renderText() {
|
||||
const {
|
||||
bodyRanges,
|
||||
deletedForEveryone,
|
||||
direction,
|
||||
i18n,
|
||||
openConversation,
|
||||
status,
|
||||
text,
|
||||
textPending,
|
||||
|
@ -1075,8 +1082,11 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
)}
|
||||
>
|
||||
<MessageBody
|
||||
text={contents || ''}
|
||||
bodyRanges={bodyRanges}
|
||||
direction={direction}
|
||||
i18n={i18n}
|
||||
openConversation={openConversation}
|
||||
text={contents || ''}
|
||||
textPending={textPending}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -14,11 +14,13 @@ const i18n = setupI18n('en', enMessages);
|
|||
const story = storiesOf('Components/Conversation/MessageBody', module);
|
||||
|
||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
bodyRanges: overrideProps.bodyRanges,
|
||||
disableJumbomoji: boolean(
|
||||
'disableJumbomoji',
|
||||
overrideProps.disableJumbomoji || false
|
||||
),
|
||||
disableLinks: boolean('disableLinks', overrideProps.disableLinks || false),
|
||||
direction: 'incoming',
|
||||
i18n,
|
||||
text: text('text', overrideProps.text || ''),
|
||||
textPending: boolean('textPending', overrideProps.textPending || false),
|
||||
|
@ -92,3 +94,78 @@ story.add('Text Pending', () => {
|
|||
|
||||
return <MessageBody {...props} />;
|
||||
});
|
||||
|
||||
story.add('@Mention', () => {
|
||||
const props = createProps({
|
||||
bodyRanges: [
|
||||
{
|
||||
start: 5,
|
||||
length: 1,
|
||||
mentionUuid: 'tuv',
|
||||
replacementText: 'Bender B Rodriguez 🤖',
|
||||
},
|
||||
],
|
||||
text:
|
||||
'Like \uFFFC once said: My story is a lot like yours, only more interesting because it involves robots',
|
||||
});
|
||||
|
||||
return <MessageBody {...props} />;
|
||||
});
|
||||
|
||||
story.add('Multiple @Mentions', () => {
|
||||
const props = createProps({
|
||||
bodyRanges: [
|
||||
{
|
||||
start: 4,
|
||||
length: 1,
|
||||
mentionUuid: 'abc',
|
||||
replacementText: 'Professor Farnsworth',
|
||||
},
|
||||
{
|
||||
start: 2,
|
||||
length: 1,
|
||||
mentionUuid: 'def',
|
||||
replacementText: 'Philip J Fry',
|
||||
},
|
||||
{
|
||||
start: 0,
|
||||
length: 1,
|
||||
mentionUuid: 'xyz',
|
||||
replacementText: 'Yancy Fry',
|
||||
},
|
||||
],
|
||||
text: '\uFFFC \uFFFC \uFFFC',
|
||||
});
|
||||
|
||||
return <MessageBody {...props} />;
|
||||
});
|
||||
|
||||
story.add('Complex MessageBody', () => {
|
||||
const props = createProps({
|
||||
bodyRanges: [
|
||||
{
|
||||
start: 80,
|
||||
length: 1,
|
||||
mentionUuid: 'xox',
|
||||
replacementText: 'Cereal Killer',
|
||||
},
|
||||
{
|
||||
start: 78,
|
||||
length: 1,
|
||||
mentionUuid: 'wer',
|
||||
replacementText: 'Acid Burn',
|
||||
},
|
||||
{
|
||||
start: 4,
|
||||
length: 1,
|
||||
mentionUuid: 'ldo',
|
||||
replacementText: 'Zero Cool',
|
||||
},
|
||||
],
|
||||
direction: 'outgoing',
|
||||
text:
|
||||
'Hey \uFFFC\nCheck out https://www.signal.org I think you will really like it 😍\n\ncc \uFFFC \uFFFC',
|
||||
});
|
||||
|
||||
return <MessageBody {...props} />;
|
||||
});
|
||||
|
|
|
@ -1,13 +1,24 @@
|
|||
import React from 'react';
|
||||
|
||||
import { getSizeClass, SizeClassType } from '../emoji/lib';
|
||||
import { AtMentionify } from './AtMentionify';
|
||||
import { Emojify } from './Emojify';
|
||||
import { AddNewLines } from './AddNewLines';
|
||||
import { Linkify } from './Linkify';
|
||||
|
||||
import { LocalizerType, RenderTextCallbackType } from '../../types/Util';
|
||||
import {
|
||||
BodyRangesType,
|
||||
LocalizerType,
|
||||
RenderTextCallbackType,
|
||||
} from '../../types/Util';
|
||||
|
||||
type OpenConversationActionType = (
|
||||
conversationId: string,
|
||||
messageId?: string
|
||||
) => void;
|
||||
|
||||
export interface Props {
|
||||
direction?: 'incoming' | 'outgoing';
|
||||
text: string;
|
||||
textPending?: boolean;
|
||||
/** If set, all emoji will be the same size. Otherwise, just one emoji will be large. */
|
||||
|
@ -15,13 +26,10 @@ export interface Props {
|
|||
/** If set, links will be left alone instead of turned into clickable `<a>` tags. */
|
||||
disableLinks?: boolean;
|
||||
i18n: LocalizerType;
|
||||
bodyRanges?: BodyRangesType;
|
||||
openConversation?: OpenConversationActionType;
|
||||
}
|
||||
|
||||
const renderNewLines: RenderTextCallbackType = ({
|
||||
text: textWithNewLines,
|
||||
key,
|
||||
}) => <AddNewLines key={key} text={textWithNewLines} />;
|
||||
|
||||
const renderEmoji = ({
|
||||
text,
|
||||
key,
|
||||
|
@ -49,6 +57,27 @@ const renderEmoji = ({
|
|||
* them for you.
|
||||
*/
|
||||
export class MessageBody extends React.Component<Props> {
|
||||
private readonly renderNewLines: RenderTextCallbackType = ({
|
||||
text: textWithNewLines,
|
||||
key,
|
||||
}) => {
|
||||
const { bodyRanges, direction, openConversation } = this.props;
|
||||
return (
|
||||
<AddNewLines
|
||||
key={key}
|
||||
text={textWithNewLines}
|
||||
renderNonNewLine={({ text }) => (
|
||||
<AtMentionify
|
||||
direction={direction}
|
||||
text={text}
|
||||
bodyRanges={bodyRanges}
|
||||
openConversation={openConversation}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
public addDownloading(jsx: JSX.Element): JSX.Element {
|
||||
const { i18n, textPending } = this.props;
|
||||
|
||||
|
@ -67,6 +96,7 @@ export class MessageBody extends React.Component<Props> {
|
|||
|
||||
public render() {
|
||||
const {
|
||||
bodyRanges,
|
||||
text,
|
||||
textPending,
|
||||
disableJumbomoji,
|
||||
|
@ -74,7 +104,10 @@ export class MessageBody extends React.Component<Props> {
|
|||
i18n,
|
||||
} = this.props;
|
||||
const sizeClass = disableJumbomoji ? undefined : getSizeClass(text);
|
||||
const textWithPending = textPending ? `${text}...` : text;
|
||||
const textWithPending = AtMentionify.preprocessMentions(
|
||||
textPending ? `${text}...` : text,
|
||||
bodyRanges
|
||||
);
|
||||
|
||||
if (disableLinks) {
|
||||
return this.addDownloading(
|
||||
|
@ -83,7 +116,7 @@ export class MessageBody extends React.Component<Props> {
|
|||
text: textWithPending,
|
||||
sizeClass,
|
||||
key: 0,
|
||||
renderNonEmoji: renderNewLines,
|
||||
renderNonEmoji: this.renderNewLines,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -97,7 +130,7 @@ export class MessageBody extends React.Component<Props> {
|
|||
text: nonLinkText,
|
||||
sizeClass,
|
||||
key,
|
||||
renderNonEmoji: renderNewLines,
|
||||
renderNonEmoji: this.renderNewLines,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -106,6 +106,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
isIncoming: boolean('isIncoming', overrideProps.isIncoming || false),
|
||||
onClick: action('onClick'),
|
||||
onClose: action('onClose'),
|
||||
openConversation: action('openConversation'),
|
||||
referencedMessageNotFound: boolean(
|
||||
'referencedMessageNotFound',
|
||||
overrideProps.referencedMessageNotFound || false
|
||||
|
@ -358,3 +359,41 @@ story.add('Missing Text & Attachment', () => {
|
|||
|
||||
return <Quote {...props} />;
|
||||
});
|
||||
|
||||
story.add('@mention + outgoing + another author', () => {
|
||||
const props = createProps({
|
||||
authorTitle: 'Tony Stark',
|
||||
text: '@Captain America Lunch later?',
|
||||
});
|
||||
|
||||
return <Quote {...props} />;
|
||||
});
|
||||
|
||||
story.add('@mention + outgoing + me', () => {
|
||||
const props = createProps({
|
||||
isFromMe: true,
|
||||
text: '@Captain America Lunch later?',
|
||||
});
|
||||
|
||||
return <Quote {...props} />;
|
||||
});
|
||||
|
||||
story.add('@mention + incoming + another author', () => {
|
||||
const props = createProps({
|
||||
authorTitle: 'Captain America',
|
||||
isIncoming: true,
|
||||
text: '@Tony Stark sure',
|
||||
});
|
||||
|
||||
return <Quote {...props} />;
|
||||
});
|
||||
|
||||
story.add('@mention + incoming + me', () => {
|
||||
const props = createProps({
|
||||
isFromMe: true,
|
||||
isIncoming: true,
|
||||
text: '@Tony Stark sure',
|
||||
});
|
||||
|
||||
return <Quote {...props} />;
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@ import * as MIME from '../../../ts/types/MIME';
|
|||
import * as GoogleChrome from '../../../ts/util/GoogleChrome';
|
||||
|
||||
import { MessageBody } from './MessageBody';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { BodyRangesType, LocalizerType } from '../../types/Util';
|
||||
import { ColorType } from '../../types/Colors';
|
||||
import { ContactName } from './ContactName';
|
||||
|
||||
|
@ -18,12 +18,14 @@ export interface Props {
|
|||
authorProfileName?: string;
|
||||
authorName?: string;
|
||||
authorColor?: ColorType;
|
||||
bodyRanges?: BodyRangesType;
|
||||
i18n: LocalizerType;
|
||||
isFromMe: boolean;
|
||||
isIncoming: boolean;
|
||||
withContentAbove: boolean;
|
||||
onClick?: () => void;
|
||||
onClose?: () => void;
|
||||
openConversation: (conversationId: string, messageId?: string) => void;
|
||||
text: string;
|
||||
referencedMessageNotFound: boolean;
|
||||
}
|
||||
|
@ -228,8 +230,15 @@ export class Quote extends React.Component<Props, State> {
|
|||
return null;
|
||||
}
|
||||
|
||||
public renderText() {
|
||||
const { i18n, text, attachment, isIncoming } = this.props;
|
||||
public renderText(): JSX.Element | null {
|
||||
const {
|
||||
bodyRanges,
|
||||
i18n,
|
||||
text,
|
||||
attachment,
|
||||
isIncoming,
|
||||
openConversation,
|
||||
} = this.props;
|
||||
|
||||
if (text) {
|
||||
return (
|
||||
|
@ -240,7 +249,13 @@ export class Quote extends React.Component<Props, State> {
|
|||
isIncoming ? 'module-quote__primary__text--incoming' : null
|
||||
)}
|
||||
>
|
||||
<MessageBody text={text} disableLinks={true} i18n={i18n} />
|
||||
<MessageBody
|
||||
disableLinks
|
||||
text={text}
|
||||
i18n={i18n}
|
||||
bodyRanges={bodyRanges}
|
||||
openConversation={openConversation}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
8
ts/textsecure.d.ts
vendored
8
ts/textsecure.d.ts
vendored
|
@ -577,6 +577,7 @@ export declare namespace DataMessageClass {
|
|||
static VIEW_ONCE: number;
|
||||
static VIEW_ONCE_VIDEO: number;
|
||||
static REACTIONS: number;
|
||||
static MENTIONS: number;
|
||||
static CURRENT: number;
|
||||
}
|
||||
|
||||
|
@ -587,6 +588,13 @@ export declare namespace DataMessageClass {
|
|||
authorUuid?: string;
|
||||
text?: string;
|
||||
attachments?: Array<DataMessageClass.Quote.QuotedAttachment>;
|
||||
bodyRanges?: Array<DataMessageClass.BodyRange>;
|
||||
}
|
||||
|
||||
class BodyRange {
|
||||
start?: number;
|
||||
length?: number;
|
||||
mentionUuid?: string;
|
||||
}
|
||||
|
||||
class Reaction {
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
StorageServiceCredentials,
|
||||
} from '../textsecure.d';
|
||||
import { MessageError, SignedPreKeyRotationError } from './Errors';
|
||||
import { BodyRangesType } from '../types/Util';
|
||||
|
||||
function stringToArrayBuffer(str: string): ArrayBuffer {
|
||||
if (typeof str !== 'string') {
|
||||
|
@ -258,7 +259,7 @@ class Message {
|
|||
}
|
||||
if (this.quote) {
|
||||
const { QuotedAttachment } = window.textsecure.protobuf.DataMessage.Quote;
|
||||
const { Quote } = window.textsecure.protobuf.DataMessage;
|
||||
const { BodyRange, Quote } = window.textsecure.protobuf.DataMessage;
|
||||
|
||||
proto.quote = new Quote();
|
||||
const { quote } = proto;
|
||||
|
@ -279,6 +280,14 @@ class Message {
|
|||
return quotedAttachment;
|
||||
}
|
||||
);
|
||||
const bodyRanges: BodyRangesType = this.quote.bodyRanges || [];
|
||||
quote.bodyRanges = bodyRanges.map(range => {
|
||||
const bodyRange = new BodyRange();
|
||||
bodyRange.start = range.start;
|
||||
bodyRange.length = range.length;
|
||||
bodyRange.mentionUuid = range.mentionUuid;
|
||||
return bodyRange;
|
||||
});
|
||||
}
|
||||
if (this.expireTimer) {
|
||||
proto.expireTimer = this.expireTimer;
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
export type BodyRangesType = Array<{
|
||||
start: number;
|
||||
length: number;
|
||||
mentionUuid: string;
|
||||
replacementText: string;
|
||||
conversationID?: string;
|
||||
}>;
|
||||
|
||||
export type RenderTextCallbackType = (options: {
|
||||
text: string;
|
||||
key: number;
|
||||
|
|
|
@ -13069,7 +13069,7 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Message.tsx",
|
||||
"line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();",
|
||||
"lineNumber": 210,
|
||||
"lineNumber": 212,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-08-28T19:36:40.817Z"
|
||||
},
|
||||
|
@ -13077,7 +13077,7 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Message.tsx",
|
||||
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
||||
"lineNumber": 211,
|
||||
"lineNumber": 213,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-09-11T17:24:56.124Z",
|
||||
"reasonDetail": "Used for managing focus only"
|
||||
|
@ -13086,7 +13086,7 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Message.tsx",
|
||||
"line": " > = React.createRef();",
|
||||
"lineNumber": 214,
|
||||
"lineNumber": 216,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-08-28T19:36:40.817Z"
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue