Modern profile sharing in 1:1 and GroupV1 groups

This commit is contained in:
Scott Nonnenberg 2020-10-16 11:31:57 -07:00
parent 60f2422e2a
commit 04b7a29229
22 changed files with 371 additions and 115 deletions

View file

@ -239,9 +239,16 @@ export class ConversationModel extends window.Backbone.Model<
// Keep props ready
this.generateProps = () => {
// This is to prevent race conditions on startup; Conversation models are created
// but the full window.ConversationController.load() sequence isn't complete.
if (!window.ConversationController.isFetchComplete()) {
return;
}
this.cachedProps = this.getProps();
};
this.on('change', this.generateProps);
this.generateProps();
}
@ -1027,19 +1034,13 @@ export class ConversationModel extends window.Backbone.Model<
});
}
format(): ConversationType | null | undefined {
format(): ConversationType {
this.cachedProps = this.cachedProps || this.getProps();
return this.cachedProps;
}
getProps(): ConversationType | null {
// This is to prevent race conditions on startup; Conversation models are created
// but the full window.ConversationController.load() sequence isn't complete. So, we
// don't cache props on create, but we do later when load() calls generateProps()
// for us.
if (!window.ConversationController.isFetchComplete()) {
return null;
}
getProps(): ConversationType {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const color = this.getColor()!;
@ -1064,6 +1065,13 @@ export class ConversationModel extends window.Backbone.Model<
'desktop.messageRequests'
);
let groupVersion: undefined | 1 | 2;
if (this.isGroupV1()) {
groupVersion = 1;
} else if (this.isGroupV2()) {
groupVersion = 2;
}
// TODO: DESKTOP-720
/* eslint-disable @typescript-eslint/no-non-null-assertion */
const result = {
@ -1078,12 +1086,14 @@ export class ConversationModel extends window.Backbone.Model<
draftPreview,
draftText,
firstName: this.get('profileName')!,
groupVersion,
inboxPosition,
isAccepted: this.getAccepted(),
isArchived: this.get('isArchived')!,
isBlocked: this.isBlocked(),
isMe: this.isMe(),
isPinned: this.get('isPinned'),
isMissingMandatoryProfileSharing: this.isMissingRequiredProfileSharing(),
isVerified: this.isVerified(),
lastMessage: {
status: this.get('lastMessageStatus')!,
@ -1091,6 +1101,7 @@ export class ConversationModel extends window.Backbone.Model<
deletedForEveryone: this.get('lastMessageDeletedForEveryone')!,
},
lastUpdated: this.get('timestamp')!,
membersCount: this.isPrivate()
? undefined
: (this.get('membersV2')! || this.get('members')! || []).length,
@ -1693,6 +1704,18 @@ export class ConversationModel extends window.Backbone.Model<
return this.get('messageRequestResponseType') || 0;
}
isMissingRequiredProfileSharing(): boolean {
const mandatoryProfileSharingEnabled = window.Signal.RemoteConfig.isEnabled(
'desktop.mandatoryProfileSharing'
);
if (!mandatoryProfileSharingEnabled) {
return false;
}
return !this.get('profileSharing');
}
/**
* Determine if this conversation should be considered "accepted" in terms
* of message requests
@ -3864,20 +3887,19 @@ export class ConversationModel extends window.Backbone.Model<
}
const { migrateColor } = Util;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return migrateColor(this.get('color')!);
return migrateColor(this.get('color'));
}
getAvatarPath(): string | null {
getAvatarPath(): string | undefined {
const avatar = this.isMe()
? this.get('profileAvatar') || this.get('avatar')
: this.get('avatar') || this.get('profileAvatar');
if (avatar && avatar.path) {
return getAbsoluteAttachmentPath(avatar.path);
if (!avatar || !avatar.path) {
return undefined;
}
return null;
return getAbsoluteAttachmentPath(avatar.path);
}
canChangeTimer(): boolean {

View file

@ -8,6 +8,7 @@ import {
LastMessageStatus,
ConversationType,
} from '../state/ducks/conversations';
import { PropsData } from '../components/conversation/Message';
import { CallbackResultType } from '../textsecure/SendMessage';
import { BodyRangesType } from '../types/Util';
import { PropsDataType as GroupsV2Props } from '../components/conversation/GroupV2Change';
@ -59,7 +60,9 @@ const { getTextWithMentions, GoogleChrome } = window.Signal.Util;
const { addStickerPackReference, getMessageBySender } = window.Signal.Data;
const { bytesFromString } = window.Signal.Crypto;
const PLACEHOLDER_CONTACT = {
const PLACEHOLDER_CONTACT: Pick<ConversationType, 'title' | 'type' | 'id'> = {
id: 'placeholder-contact',
type: 'direct',
title: window.i18n('unknownContact'),
};
@ -694,26 +697,26 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
.map(attachment => this.getPropsForAttachment(attachment));
}
getPropsForMessage(): WhatIsThis {
// Note: interactionMode is mixed in via selectors/conversations._messageSelector
getPropsForMessage(): Omit<PropsData, 'interactionMode'> {
const sourceId = this.getContactId();
const contact = this.findAndFormatContact(sourceId);
const contactModel = this.findContact(sourceId);
const authorColor = contactModel ? contactModel.getColor() : null;
const authorAvatarPath = contactModel ? contactModel.getAvatarPath() : null;
const authorColor = contactModel ? contactModel.getColor() : undefined;
const authorAvatarPath = contactModel
? contactModel.getAvatarPath()
: undefined;
const expirationLength = this.get('expireTimer') * 1000;
const expireTimerStart = this.get('expirationStartTimestamp');
const expirationTimestamp =
expirationLength && expireTimerStart
? expireTimerStart + expirationLength
: null;
: undefined;
const conversation = this.getConversation();
const isGroup = conversation && !conversation.isPrivate();
const conversationAccepted = Boolean(
conversation && conversation.getAccepted()
);
const sticker = this.get('sticker');
const isTapToView = this.isTapToView();
@ -739,7 +742,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
textPending: this.get('bodyPending'),
id: this.id,
conversationId: this.get('conversationId'),
conversationAccepted,
isSticker: Boolean(sticker),
direction: this.isIncoming() ? 'incoming' : 'outgoing',
timestamp: this.get('sent_at'),
@ -747,6 +749,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
contact: this.getPropsForEmbeddedContact(),
canReply: this.canReply(),
canDeleteForEveryone: this.canDeleteForEveryone(),
canDownload: this.canDownload(),
authorTitle: contact.title,
authorColor,
authorName: contact.name,
@ -801,7 +804,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
// Dependencies of prop-generation functions
findAndFormatContact(
identifier?: string
): Partial<ConversationType> & Pick<ConversationType, 'title'> {
): Partial<ConversationType> &
Pick<ConversationType, 'title' | 'id' | 'type'> {
if (!identifier) {
return PLACEHOLDER_CONTACT;
}
@ -824,6 +828,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
});
return {
id: 'phone-only',
type: 'direct',
title: phoneNumber,
phoneNumber,
};
@ -839,9 +845,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}
// eslint-disable-next-line class-methods-use-this
createNonBreakingLastSeparator(text: string): string | null {
createNonBreakingLastSeparator(text: string): string | undefined {
if (!text) {
return null;
return undefined;
}
const nbsp = '\xa0';
@ -859,7 +865,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return this.get('type') === 'incoming';
}
getMessagePropStatus(): LastMessageStatus | null {
getMessagePropStatus(): LastMessageStatus | undefined {
const sent = this.get('sent');
const sentTo = this.get('sent_to') || [];
@ -870,7 +876,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return 'error';
}
if (!this.isOutgoing()) {
return null;
return undefined;
}
const readBy = this.get('read_by') || [];
@ -2010,34 +2016,50 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return true;
}
canDownload(): boolean {
const conversation = this.getConversation();
const isAccepted = Boolean(conversation && conversation.getAccepted());
if (this.isOutgoing()) {
return true;
}
return isAccepted;
}
canReply(): boolean {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const isAccepted = this.getConversation()!.getAccepted();
const conversation = this.getConversation();
const errors = this.get('errors');
const isOutgoing = this.get('type') === 'outgoing';
const numDelivered = this.get('delivered');
// Case 1: We cannot reply if we have accepted the message request
if (!isAccepted) {
// Case 1: If mandatory profile sharing is enabled, and we haven't shared yet, then
// we can't reply.
if (conversation?.isMissingRequiredProfileSharing()) {
return false;
}
// Case 2: We cannot reply if this message is deleted for everyone
// Case 2: We cannot reply if we have accepted the message request
if (!conversation?.getAccepted()) {
return false;
}
// Case 3: We cannot reply if this message is deleted for everyone
if (this.get('deletedForEveryone')) {
return false;
}
// Case 3: We can reply if this is outgoing and delievered to at least one recipient
// Case 4: We can reply if this is outgoing and delievered to at least one recipient
if (isOutgoing && numDelivered > 0) {
return true;
}
// Case 4: We can reply if there are no errors
// Case 5: We can reply if there are no errors
if (!errors || (errors && errors.length === 0)) {
return true;
}
// Case 5: default
// Case 6: default
return false;
}