Add support for mentions in private group chats

This commit is contained in:
Maxim Shishmarev 2019-10-18 16:52:59 +11:00
parent 90f1d4a6aa
commit f5e9a870f7
9 changed files with 154 additions and 42 deletions

View file

@ -573,8 +573,12 @@
? expireTimerStart + expirationLength
: null;
// TODO: investigate why conversation is undefined
// for the public group chat
const conversation = this.getConversation();
const isGroup = conversation && !conversation.isPrivate();
const convoId = conversation ? conversation.id : undefined;
const isGroup = !!conversation && !conversation.isPrivate();
const attachments = this.get('attachments') || [];
const firstAttachment = attachments[0];
@ -592,6 +596,7 @@
authorProfileName: contact.profileName,
authorPhoneNumber: contact.phoneNumber,
conversationType: isGroup ? 'group' : 'direct',
convoId,
attachments: attachments
.filter(attachment => !attachment.error)
.map(attachment => this.getPropsForAttachment(attachment)),

View file

@ -2389,7 +2389,7 @@
// Note: schedule the member list handler shortly afterwards, so
// that the input element has time to update its cursor position to
// what the user would expect
if (this.model.isPublic()) {
if (this.model.get('type') === 'group') {
window.requestAnimationFrame(this.maybeShowMembers.bind(this, event));
}
@ -2521,10 +2521,31 @@
return query;
};
let allMembers = window.lokiPublicChatAPI.getListOfMembers();
allMembers = allMembers.filter(d => !!d);
allMembers = allMembers.filter(d => d.authorProfileName !== 'Anonymous');
allMembers = _.uniq(allMembers, true, d => d.authorPhoneNumber);
let allMembers;
if (this.model.isPublic()) {
let members = window.lokiPublicChatAPI.getListOfMembers();
members = members.filter(d => !!d);
members = members.filter(d => d.authorProfileName !== 'Anonymous');
allMembers = _.uniq(members, true, d => d.authorPhoneNumber);
} else {
const members = this.model.get('members');
if (!members || members.length === 0) {
return;
}
const privateConvos = window
.getConversations()
.models.filter(d => d.isPrivate());
const memberConvos = members
.map(m => privateConvos.find(c => c.id === m))
.filter(c => !!c);
allMembers = memberConvos.map(m => ({
id: m.id,
authorPhoneNumber: m.id,
authorProfileName: m.getLokiProfile().displayName,
}));
}
const cursorPos = event.target.selectionStart;

View file

@ -8,12 +8,14 @@ declare global {
lokiPublicChatAPI: any;
shortenPubkey: any;
pubkeyPattern: any;
getConversations: any;
}
}
interface MentionProps {
key: number;
text: string;
convoId: string;
}
interface MentionState {
@ -78,13 +80,41 @@ class Mention extends React.Component<MentionProps, MentionState> {
}
private findMember(pubkey: String) {
const members = window.lokiPublicChatAPI.getListOfMembers();
if (!members) {
return null;
}
const filtered = members.filter((m: any) => !!m);
let groupMembers;
return filtered.find(
const groupConvos = window.getConversations().models.filter((d: any) => {
return !d.isPrivate();
});
const thisConvo = groupConvos.find((d: any) => {
return d.id === this.props.convoId;
});
if (thisConvo.isPublic()) {
// TODO: make this work for other public chats as well
groupMembers = window.lokiPublicChatAPI
.getListOfMembers()
.filter((m: any) => !!m);
} else {
const privateConvos = window
.getConversations()
.models.filter((d: any) => d.isPrivate());
const members = thisConvo.attributes.members;
if (!members) {
return null;
}
const memberConversations = members
.map((m: any) => privateConvos.find((c: any) => c.id === m))
.filter((c: any) => !!c);
groupMembers = memberConversations.map((m: any) => {
return {
id: m.id,
authorPhoneNumber: m.id,
authorProfileName: m.getLokiProfile().displayName,
};
});
}
return groupMembers.find(
({ authorPhoneNumber: pn }: any) => pn && pn === pubkey
);
}
@ -93,6 +123,7 @@ class Mention extends React.Component<MentionProps, MentionState> {
interface Props {
text: string;
renderOther?: RenderTextCallbackType;
convoId: string;
}
export class AddMentions extends React.Component<Props> {
@ -101,7 +132,7 @@ export class AddMentions extends React.Component<Props> {
};
public render() {
const { text, renderOther } = this.props;
const { text, renderOther, convoId } = this.props;
const results: Array<any> = [];
const FIND_MENTIONS = window.pubkeyPattern;
@ -126,7 +157,7 @@ export class AddMentions extends React.Component<Props> {
}
const pubkey = text.slice(match.index, FIND_MENTIONS.lastIndex);
results.push(<Mention text={pubkey} key={count++} />);
results.push(<Mention text={pubkey} key={count++} convoId={convoId} />);
// @ts-ignore
last = FIND_MENTIONS.lastIndex;

View file

@ -6,6 +6,7 @@ interface Props {
text: string;
/** Allows you to customize now non-newlines are rendered. Simplest is just a <span>. */
renderNonNewLine?: RenderTextCallbackType;
convoId: string;
}
export class AddNewLines extends React.Component<Props> {
@ -14,7 +15,7 @@ export class AddNewLines extends React.Component<Props> {
};
public render() {
const { text, renderNonNewLine } = this.props;
const { text, renderNonNewLine, convoId } = this.props;
const results: Array<any> = [];
const FIND_NEWLINES = /\n/g;
@ -29,14 +30,14 @@ export class AddNewLines extends React.Component<Props> {
let count = 1;
if (!match) {
return renderNonNewLine({ text, key: 0 });
return renderNonNewLine({ text, key: 0, convoId });
}
while (match) {
if (last < match.index) {
const textWithNoNewline = text.slice(last, match.index);
results.push(
renderNonNewLine({ text: textWithNoNewline, key: count++ })
renderNonNewLine({ text: textWithNoNewline, key: count++, convoId })
);
}
@ -48,7 +49,9 @@ export class AddNewLines extends React.Component<Props> {
}
if (last < text.length) {
results.push(renderNonNewLine({ text: text.slice(last), key: count++ }));
results.push(
renderNonNewLine({ text: text.slice(last), key: count++, convoId })
);
}
return results;

View file

@ -56,17 +56,25 @@ interface Props {
/** Allows you to customize now non-newlines are rendered. Simplest is just a <span>. */
renderNonEmoji?: RenderTextCallbackType;
i18n: LocalizerType;
isPublic?: boolean;
isGroup?: boolean;
convoId: string;
}
export class Emojify extends React.Component<Props> {
public static defaultProps: Partial<Props> = {
renderNonEmoji: ({ text }) => text || '',
isPublic: false,
isGroup: false,
};
public render() {
const { text, sizeClass, renderNonEmoji, i18n, isPublic } = this.props;
const {
text,
sizeClass,
renderNonEmoji,
i18n,
isGroup,
convoId,
} = this.props;
const results: Array<any> = [];
const regex = getRegex();
@ -81,14 +89,19 @@ export class Emojify extends React.Component<Props> {
let count = 1;
if (!match) {
return renderNonEmoji({ text, key: 0, isPublic });
return renderNonEmoji({ text, key: 0, isGroup, convoId });
}
while (match) {
if (last < match.index) {
const textWithNoEmoji = text.slice(last, match.index);
results.push(
renderNonEmoji({ text: textWithNoEmoji, key: count++, isPublic })
renderNonEmoji({
text: textWithNoEmoji,
key: count++,
isGroup,
convoId,
})
);
}
@ -100,7 +113,12 @@ export class Emojify extends React.Component<Props> {
if (last < text.length) {
results.push(
renderNonEmoji({ text: text.slice(last), key: count++, isPublic })
renderNonEmoji({
text: text.slice(last),
key: count++,
isGroup,
convoId,
})
);
}

View file

@ -93,6 +93,7 @@ export interface Props {
isExpired: boolean;
expirationLength?: number;
expirationTimestamp?: number;
convoId: string;
isP2p?: boolean;
isPublic?: boolean;
isRss?: boolean;
@ -590,6 +591,7 @@ export class Message extends React.PureComponent<Props, State> {
i18n,
quote,
isPublic,
convoId,
} = this.props;
if (!quote) {
@ -614,6 +616,8 @@ export class Message extends React.PureComponent<Props, State> {
text={quote.text}
attachment={quote.attachment}
isIncoming={direction === 'incoming'}
conversationType={conversationType}
convoId={convoId}
isPublic={isPublic}
authorPhoneNumber={displayedPubkey}
authorProfileName={quote.authorProfileName}
@ -718,7 +722,16 @@ export class Message extends React.PureComponent<Props, State> {
}
public renderText() {
const { text, textPending, i18n, direction, status, isRss } = this.props;
const {
text,
textPending,
i18n,
direction,
status,
isRss,
conversationType,
convoId,
} = this.props;
const contents =
direction === 'incoming' && status === 'error'
@ -745,7 +758,8 @@ export class Message extends React.PureComponent<Props, State> {
isRss={isRss}
i18n={i18n}
textPending={textPending}
isPublic={this.props.isPublic}
isGroup={conversationType === 'group'}
convoId={convoId}
/>
</div>
);

View file

@ -16,12 +16,13 @@ interface Props {
disableJumbomoji?: boolean;
/** If set, links will be left alone instead of turned into clickable `<a>` tags. */
disableLinks?: boolean;
isPublic?: boolean;
isGroup?: boolean;
i18n: LocalizerType;
convoId: string;
}
const renderMentions: RenderTextCallbackType = ({ text, key }) => (
<AddMentions key={key} text={text} />
// eslint-disable-next-line
const renderMentions: RenderTextCallbackType = ({ text, key, convoId }) => (
<AddMentions key={key} text={text} convoId={convoId} />
);
const renderDefault: RenderTextCallbackType = ({ text }) => text;
@ -29,15 +30,17 @@ const renderDefault: RenderTextCallbackType = ({ text }) => text;
const renderNewLines: RenderTextCallbackType = ({
text: textWithNewLines,
key,
isPublic,
isGroup,
convoId,
}) => {
const renderOther = isPublic ? renderMentions : renderDefault;
const renderOther = isGroup ? renderMentions : renderDefault;
return (
<AddNewLines
key={key}
text={textWithNewLines}
renderNonNewLine={renderOther}
convoId={convoId}
/>
);
};
@ -48,14 +51,16 @@ const renderEmoji = ({
key,
sizeClass,
renderNonEmoji,
isPublic,
isGroup,
convoId,
}: {
i18n: LocalizerType;
text: string;
key: number;
sizeClass?: SizeClassType;
renderNonEmoji: RenderTextCallbackType;
isPublic?: boolean;
isGroup?: boolean;
convoId?: string;
}) => (
<Emojify
i18n={i18n}
@ -63,7 +68,8 @@ const renderEmoji = ({
text={text}
sizeClass={sizeClass}
renderNonEmoji={renderNonEmoji}
isPublic={isPublic}
isGroup={isGroup}
convoId={convoId}
/>
);
@ -75,7 +81,7 @@ const renderEmoji = ({
*/
export class MessageBody extends React.Component<Props> {
public static defaultProps: Partial<Props> = {
isPublic: false,
isGroup: false,
};
public addDownloading(jsx: JSX.Element): JSX.Element {
@ -102,7 +108,8 @@ export class MessageBody extends React.Component<Props> {
disableLinks,
isRss,
i18n,
isPublic,
isGroup,
convoId,
} = this.props;
const sizeClass = disableJumbomoji ? undefined : getSizeClass(text);
const textWithPending = textPending ? `${text}...` : text;
@ -115,7 +122,8 @@ export class MessageBody extends React.Component<Props> {
sizeClass,
key: 0,
renderNonEmoji: renderNewLines,
isPublic,
isGroup,
convoId,
})
);
}
@ -131,7 +139,8 @@ export class MessageBody extends React.Component<Props> {
sizeClass,
key,
renderNonEmoji: renderNewLines,
isPublic,
isGroup,
convoId,
});
}}
/>

View file

@ -19,6 +19,8 @@ interface Props {
i18n: LocalizerType;
isFromMe: boolean;
isIncoming: boolean;
conversationType: 'group' | 'direct';
convoId: string;
isPublic?: boolean;
withContentAbove: boolean;
onClick?: () => void;
@ -215,7 +217,14 @@ export class Quote extends React.Component<Props, State> {
}
public renderText() {
const { i18n, text, attachment, isIncoming, isPublic } = this.props;
const {
i18n,
text,
attachment,
isIncoming,
conversationType,
convoId,
} = this.props;
if (text) {
return (
@ -227,7 +236,8 @@ export class Quote extends React.Component<Props, State> {
)}
>
<MessageBody
isPublic={isPublic}
isGroup={conversationType === 'group'}
convoId={convoId}
text={text}
disableLinks={true}
i18n={i18n}

View file

@ -2,7 +2,8 @@ export type RenderTextCallbackType = (
options: {
text: string;
key: number;
isPublic?: boolean;
isGroup?: boolean;
convoId?: string;
}
) => JSX.Element | string;