fix scroll to quoted message and contact display name of group updates

pull/1387/head
Audric Ackermann 4 years ago
parent 7ae79ee0a2
commit 0b4400837b
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -541,12 +541,6 @@
window.dispatchEvent(new Event('storage_ready'));
window.log.info('Cleanup: starting...');
window.getOurDisplayName = () => {
const ourNumber = window.storage.get('primaryDevicePubKey');
const conversation = ConversationController.get(ourNumber, 'private');
const profile = conversation.getLokiProfile();
return profile && profile.displayName;
};
const results = await Promise.all([
window.Signal.Data.getOutgoingWithoutExpiresAt({

@ -286,6 +286,21 @@
return this._initialPromise;
},
getContactProfileNameOrShortenedPubKey: pubKey => {
const conversation = window.ConversationController.get(pubKey);
if (!conversation) {
return pubKey;
}
return conversation.getContactProfileNameOrShortenedPubKey();
},
getContactProfileNameOrFullPubKey: pubKey => {
const conversation = window.ConversationController.get(pubKey);
if (!conversation) {
return pubKey;
}
return conversation.getContactProfileNameOrFullPubKey();
},
_handleOnline: pubKey => {
try {
const conversation = this.get(pubKey);

@ -77,6 +77,7 @@ export interface ConversationModel
) => Promise<void>;
updateGroupAdmins: any;
setLokiProfile: any;
getLokiProfile: any;
onSessionResetReceived: any;
setVerifiedDefault: any;
setVerified: any;

@ -2343,6 +2343,8 @@
// conversation still appears on the conversation list but is empty
this.set({
lastMessage: null,
unreadCount: 0,
mentionedUs: false,
});
await this.commit();
@ -2359,36 +2361,65 @@
if (this.isPrivate()) {
const profileName = this.getProfileName();
const number = this.getNumber();
const name = profileName ? `${profileName} (${number})` : number;
let name;
if (window.shortenPubKey) {
name = profileName
? `${profileName} (${window.shortenPubKey(number)})`
: number;
} else {
name = profileName ? `${profileName} (${number})` : number;
}
return this.get('name') || name;
}
return this.get('name') || 'Unknown group';
},
getProfileName() {
if (this.isPrivate() && !this.get('name')) {
return this.get('profileName');
}
return null;
},
getDisplayName() {
/**
* For a private convo, returns the loki profilename if set, or a shortened
* version of the contact pubkey.
* Throws an error if called on a group convo.
* */
getContactProfileNameOrShortenedPubKey() {
if (!this.isPrivate()) {
return this.getTitle();
throw new Error(
'getContactProfileNameOrShortenedPubKey() cannot be called with a non private convo.'
);
}
const name = this.get('name');
if (name) {
return name;
const profileName = this.get('profileName');
const pubkey = this.id;
if (pubkey === textsecure.storage.user.getNumber()) {
return i18n('you');
}
return profileName || window.shortenPubKey(pubkey);
},
/**
* For a private convo, returns the loki profilename if set, or a full length
* version of the contact pubkey.
* Throws an error if called on a group convo.
* */
getContactProfileNameOrFullPubKey() {
if (!this.isPrivate()) {
throw new Error(
'getContactProfileNameOrFullPubKey() cannot be called with a non private convo.'
);
}
const profileName = this.get('profileName');
if (profileName) {
return `${this.getNumber()} ~${profileName}`;
const pubkey = this.id;
if (pubkey === textsecure.storage.user.getNumber()) {
return i18n('you');
}
return profileName || pubkey;
},
return this.getNumber();
getProfileName() {
if (this.isPrivate() && !this.get('name')) {
return this.get('profileName');
}
return null;
},
/**
* Returns
* displayName: string;

@ -168,30 +168,24 @@
this.set(attributes);
},
getNameForNumber(number) {
const conversation = ConversationController.get(number);
if (!conversation) {
return number;
}
return conversation.getDisplayName();
},
getLokiNameForNumber(number) {
const conversation = ConversationController.get(number);
if (number === textsecure.storage.user.getNumber()) {
return i18n('you');
}
if (!conversation || !conversation.getLokiProfile()) {
return number;
}
return conversation.getLokiProfile().displayName;
},
getDescription() {
if (this.isGroupUpdate()) {
const groupUpdate = this.get('group_update');
if (groupUpdate.left === 'You') {
const ourPrimary = window.textsecure.storage.get('primaryDevicePubKey');
if (
groupUpdate.left === 'You' ||
(Array.isArray(groupUpdate.left) &&
groupUpdate.left.length === 1 &&
groupUpdate.left[0] === ourPrimary)
) {
return i18n('youLeftTheGroup');
} else if (groupUpdate.left) {
return i18n('leftTheGroup', this.getNameForNumber(groupUpdate.left));
return i18n(
'leftTheGroup',
ConversationController.getContactProfileNameOrShortenedPubKey(
groupUpdate.left
)
);
}
if (groupUpdate.kicked === 'You') {
@ -206,8 +200,8 @@
messages.push(i18n('titleIsNow', groupUpdate.name));
}
if (groupUpdate.joined && groupUpdate.joined.length) {
const names = groupUpdate.joined.map(name =>
this.getLokiNameForNumber(name)
const names = groupUpdate.joined.map(pubKey =>
ConversationController.getContactProfileNameOrFullPubKey(pubKey)
);
if (names.length > 1) {
@ -220,7 +214,7 @@
if (groupUpdate.kicked && groupUpdate.kicked.length) {
const names = _.map(
groupUpdate.kicked,
this.getNameForNumber.bind(this)
ConversationController.getContactProfileNameOrShortenedPubKey
);
if (names.length > 1) {
@ -271,7 +265,9 @@
);
const pubkeysInDesc = description.match(regex);
(pubkeysInDesc || []).forEach(pubkey => {
const displayName = this.getLokiNameForNumber(pubkey.slice(1));
const displayName = ConversationController.getContactProfileNameOrShortenedPubKey(
pubkey.slice(1)
);
if (displayName && displayName.length) {
description = description.replace(pubkey, `@${displayName}`);
}
@ -746,7 +742,6 @@
const authorPhoneNumber = format(author, {
ourRegionCode: regionCode,
});
const authorProfileName = contact ? contact.getProfileName() : null;
const authorName = contact ? contact.getName() : null;
const isFromMe = contact ? contact.id === this.OUR_NUMBER : false;
const onClick = noClick
@ -769,7 +764,7 @@
: null,
isFromMe,
authorPhoneNumber,
authorProfileName,
messageId: id,
authorName,
onClick,
referencedMessageNotFound,

@ -18,21 +18,6 @@
window.Whisper = window.Whisper || {};
const { getAbsoluteAttachmentPath } = window.Signal.Migrations;
Whisper.OriginalNotFoundToast = Whisper.ToastView.extend({
render_attributes() {
return { toastMessage: i18n('originalMessageNotFound') };
},
});
Whisper.OriginalNoLongerAvailableToast = Whisper.ToastView.extend({
render_attributes() {
return { toastMessage: i18n('originalMessageNotAvailable') };
},
});
Whisper.FoundButNotLoadedToast = Whisper.ToastView.extend({
render_attributes() {
return { toastMessage: i18n('messageFoundButNotLoaded') };
},
});
Whisper.VoiceNoteMustBeOnlyAttachmentToast = Whisper.ToastView.extend({
render_attributes() {
return { toastMessage: i18n('voiceNoteMustBeOnlyAttachment') };
@ -664,74 +649,6 @@
}
},
async scrollToMessage(options = {}) {
const { author, id, referencedMessageNotFound } = options;
// For simplicity's sake, we show the 'not found' toast no matter what if we were
// not able to find the referenced message when the quote was received.
if (referencedMessageNotFound) {
const toast = new Whisper.OriginalNotFoundToast();
toast.$el.appendTo(this.$el);
toast.render();
return;
}
// Look for message in memory first, which would tell us if we could scroll to it
const targetMessage = this.model.messageCollection.find(item => {
const messageAuthor = item.getContact();
if (!messageAuthor || author !== messageAuthor.id) {
return false;
}
if (id !== item.get('sent_at')) {
return false;
}
return true;
});
// If there's no message already in memory, we won't be scrolling. So we'll gather
// some more information then show an informative toast to the user.
if (!targetMessage) {
const collection = await window.Signal.Data.getMessagesBySentAt(id, {
MessageCollection: Whisper.MessageCollection,
});
const found = Boolean(
collection.find(item => {
const messageAuthor = item.getContact();
return messageAuthor && author === messageAuthor.id;
})
);
if (found) {
const toast = new Whisper.FoundButNotLoadedToast();
toast.$el.appendTo(this.$el);
toast.render();
} else {
const toast = new Whisper.OriginalNoLongerAvailableToast();
toast.$el.appendTo(this.$el);
toast.render();
}
return;
}
const databaseId = targetMessage.id;
const el = this.$(`#${databaseId}`);
if (!el || el.length === 0) {
const toast = new Whisper.OriginalNoLongerAvailableToast();
toast.$el.appendTo(this.$el);
toast.render();
window.log.info(
`Error: had target message ${id} in messageCollection, but it was not in DOM`
);
return;
}
el[0].scrollIntoView();
},
scrollToBottom() {
// If we're above the last seen indicator, we should scroll there instead
// Note: if we don't end up at the bottom of the conversation, button won't go away!

@ -22,7 +22,6 @@ describe('InboxView', () => {
textsecure.storage.user.getNumber(),
'private'
);
window.getOurDisplayName = () => 'display name test';
inboxView = new Whisper.InboxView({
model: {},
window,

@ -5,14 +5,6 @@ import classNames from 'classnames';
import { MultiDeviceProtocol } from '../../session/protocols';
import { FindMember } from '../../util';
declare global {
interface Window {
shortenPubkey: any;
pubkeyPattern: any;
getConversations: any;
}
}
interface MentionProps {
key: number;
text: string;

@ -38,17 +38,6 @@ import _ from 'lodash';
import { animation, contextMenu, Item, Menu } from 'react-contexify';
import uuid from 'uuid';
declare global {
interface Window {
shortenPubkey: any;
contextMenuShown: boolean;
}
}
interface Trigger {
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
}
// Same as MIN_WIDTH in ImageGrid.tsx
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
@ -91,7 +80,8 @@ export interface Props {
authorPhoneNumber: string;
authorProfileName?: string;
authorName?: string;
onClick?: () => void;
messageId?: string;
onClick: (data: any) => void;
referencedMessageNotFound: boolean;
};
previews: Array<LinkPreviewType>;
@ -623,6 +613,8 @@ export class Message extends React.PureComponent<Props, State> {
quote,
isPublic,
convoId,
id,
multiSelectMode,
} = this.props;
if (!quote || !quote.authorPhoneNumber) {
@ -642,7 +634,24 @@ export class Message extends React.PureComponent<Props, State> {
return (
<Quote
i18n={window.i18n}
onClick={quote.onClick}
onClick={(e: any) => {
e.preventDefault();
e.stopPropagation();
if (multiSelectMode && id) {
this.props.onSelectMessage(id);
return;
}
const {
authorPhoneNumber,
messageId: quoteId,
referencedMessageNotFound,
} = quote;
quote?.onClick({
quoteAuthor: authorPhoneNumber,
quoteId,
referencedMessageNotFound,
});
}}
text={quote.text}
attachment={quote.attachment}
isIncoming={direction === 'incoming'}

@ -22,7 +22,7 @@ interface Props {
convoId: string;
isPublic?: boolean;
withContentAbove: boolean;
onClick?: () => void;
onClick?: (e: any) => void;
onClose?: () => void;
text: string;
referencedMessageNotFound: boolean;

@ -6,6 +6,8 @@ import { PropsData as ConversationListItemPropsType } from '../ConversationListI
import { createOrUpdateItem, getItemById } from '../../../js/modules/data';
import { APPLY_THEME } from '../../state/ducks/theme';
import { darkTheme, lightTheme } from '../../state/ducks/SessionTheme';
import { MultiDeviceProtocol } from '../../session/protocols';
import { UserUtil } from '../../util';
// tslint:disable-next-line: no-import-side-effect no-submodule-imports
export enum SectionType {
@ -154,15 +156,17 @@ class ActionsPanelPrivate extends React.Component<Props, State> {
: undefined;
if (type === SectionType.Profile) {
const pubkey = window.storage.get('primaryDevicePubKey');
const userName = window.getOurDisplayName() || pubkey;
const ourPrimary = window.storage.get('primaryDevicePubKey');
const conversation = window.ConversationController.getOrThrow(ourPrimary);
const profile = conversation.getLokiProfile();
const userName = (profile && profile.displayName) || ourPrimary;
return (
<Avatar
avatarPath={avatarPath}
size={28}
onAvatarClick={handleClick}
name={userName}
pubkey={pubkey}
pubkey={ourPrimary}
/>
);
}

@ -130,10 +130,6 @@ export class SessionConversation extends React.Component<Props, State> {
this.onClickAttachment = this.onClickAttachment.bind(this);
this.downloadAttachment = this.downloadAttachment.bind(this);
this.refreshMessages = this.refreshMessages.bind(this);
// this.getMessages = _.throttle(
// this.getMessages.bind(this),
// 1000 // one second
// );
this.getMessages = this.getMessages.bind(this);
// Keyboard navigation

@ -15,6 +15,7 @@ import { ConversationType } from '../../../state/ducks/conversations';
import { MessageModel } from '../../../../js/models/messages';
import { SessionLastSeenIndicator } from './SessionLastSeedIndicator';
import { VerificationNotification } from '../../conversation/VerificationNotification';
import { ToastUtils } from '../../../session/utils';
interface State {
isScrolledToBottom: boolean;
@ -56,6 +57,7 @@ export class SessionConversationMessagesList extends React.Component<
this.handleScroll = this.handleScroll.bind(this);
this.scrollToUnread = this.scrollToUnread.bind(this);
this.scrollToBottom = this.scrollToBottom.bind(this);
this.scrollToQuoteMessage = this.scrollToQuoteMessage.bind(this);
this.messagesEndRef = React.createRef();
this.messageContainerRef = this.props.messageContainerRef;
@ -235,6 +237,16 @@ export class SessionConversationMessagesList extends React.Component<
this.props.onDownloadAttachment({ attachment });
};
if (messageProps.quote) {
messageProps.quote.onClick = (options: {
quoteAuthor: string;
quoteId: any;
referencedMessageNotFound: boolean;
}) => {
void this.scrollToQuoteMessage(options);
};
}
return <Message {...messageProps} />;
}
@ -403,4 +415,62 @@ export class SessionConversationMessagesList extends React.Component<
public selectMessage(messageId: string) {
this.props.selectMessage(messageId);
}
private async scrollToQuoteMessage(options: any = {}) {
const { quoteAuthor, quoteId, referencedMessageNotFound } = options;
// For simplicity's sake, we show the 'not found' toast no matter what if we were
// not able to find the referenced message when the quote was received.
if (referencedMessageNotFound) {
ToastUtils.pushOriginalNotFound();
return;
}
// Look for message in memory first, which would tell us if we could scroll to it
const targetMessage = this.props.messages.find(item => {
const messageAuthor = item.propsForMessage?.authorPhoneNumber;
if (!messageAuthor || quoteAuthor !== messageAuthor) {
return false;
}
if (quoteId !== item.propsForMessage?.timestamp) {
return false;
}
return true;
});
// If there's no message already in memory, we won't be scrolling. So we'll gather
// some more information then show an informative toast to the user.
if (!targetMessage) {
const collection = await window.Signal.Data.getMessagesBySentAt(quoteId, {
MessageCollection: window.Whisper.MessageCollection,
});
const found = Boolean(
collection.find((item: MessageModel) => {
const messageAuthor = item.propsForMessage?.authorPhoneNumber;
return messageAuthor && quoteAuthor === messageAuthor;
})
);
if (found) {
ToastUtils.pushFoundButNotLoaded();
} else {
ToastUtils.pushOriginalNoLongerAvailable();
}
return;
}
const databaseId = targetMessage.id;
// const el = this.$(`#${databaseId}`);
// if (!el || el.length === 0) {
// ToastUtils.pushOriginalNoLongerAvailable();
// window.log.info(
// `Error: had target message ${id} in messageCollection, but it was not in DOM`
// );
// return;
// }
// this probably does not work for us as we need to call getMessages before
this.scrollToMessage(databaseId);
}
}

@ -25,7 +25,7 @@ export const CONVERSATION = {
DEFAULT_MESSAGE_FETCH_COUNT: 30,
MAX_MESSAGE_FETCH_COUNT: 500,
MESSAGE_FETCH_INTERVAL: 1,
// Maximum voice message duraiton of 5 minutes
// Maximum voice message duraton of 5 minutes
// which equates to 1.97 MB
MAX_VOICE_MESSAGE_DURATION: 300,
// Max attachment size: 10 MB
@ -35,7 +35,7 @@ export const CONVERSATION = {
export const UI = {
// Pixels (scroll) from the top of the top of message container
// at which more messages should be loaded
MESSAGE_CONTAINER_BUFFER_OFFSET_PX: 30,
MESSAGE_CONTAINER_BUFFER_OFFSET_PX: 1,
COLORS: {
// COMMON

@ -138,3 +138,27 @@ export function pushAudioPermissionNeeded() {
type: 'info',
});
}
export function pushOriginalNotFound() {
window.pushToast({
id: 'originalMessageNotFound',
title: window.i18n('originalMessageNotFound'),
type: 'error',
});
}
export function pushOriginalNoLongerAvailable() {
window.pushToast({
id: 'originalMessageNotAvailable',
title: window.i18n('originalMessageNotAvailable'),
type: 'error',
});
}
export function pushFoundButNotLoaded() {
window.pushToast({
id: 'messageFoundButNotLoaded',
title: window.i18n('messageFoundButNotLoaded'),
type: 'error',
});
}

6
ts/window.d.ts vendored

@ -81,9 +81,8 @@ declare global {
seedNodeList: any;
setPassword: any;
setSettingValue: any;
shortenPubkey: any;
shortenPubkey: (pubKey: string) => string;
showEditProfileDialog: any;
getOurDisplayName: () => string | undefined;
showPasswordDialog: any;
showSeedDialog: any;
storage: any;
@ -97,6 +96,8 @@ declare global {
userConfig: any;
versionInfo: any;
getStoragePubKey: any;
pubkeyPattern: any;
getConversations: any;
getGuid: any;
ContactBuffer: any;
GroupBuffer: any;
@ -106,6 +107,7 @@ declare global {
loadImage: any;
dataURLToBlobSync: any;
autoOrientImage: any;
contextMenuShown: boolean;
sessionGenerateKeyPair: (
seed: ArrayBuffer
) => Promise<{ pubKey: ArrayBufferLike; privKey: ArrayBufferLike }>;

Loading…
Cancel
Save