diff --git a/js/models/conversations.js b/js/models/conversations.js index 87843e33f..b2ec3bf66 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -2504,21 +2504,6 @@ return this.id; }, - getInitials(name) { - if (!name) { - return null; - } - - const cleaned = name.replace(/[^A-Za-z\s]+/g, '').replace(/\s+/g, ' '); - const parts = cleaned.split(' '); - const initials = parts.map(part => part.trim()[0]); - if (!initials.length) { - return null; - } - - return initials.slice(0, 2).join(''); - }, - isPrivate() { return this.get('type') === 'private'; }, @@ -2537,18 +2522,9 @@ return null; }, getAvatar() { - const title = this.get('name'); const url = this.getAvatarPath(); - if (url) { - return { url }; - } else if (this.isPrivate()) { - const symbol = this.isValid() ? '#' : '!'; - return { - content: this.getInitials(title) || symbol, - }; - } - return { url: null }; + return { url: url || null }; }, getNotificationIcon() { diff --git a/test/models/conversations_test.js b/test/models/conversations_test.js index 226e80365..4e9c741d6 100644 --- a/test/models/conversations_test.js +++ b/test/models/conversations_test.js @@ -129,12 +129,6 @@ describe('Conversation', () => { assert.equal(convo.getNumber(), ''); }); - it('has an avatar', () => { - const convo = new Whisper.ConversationCollection().add(attributes); - const avatar = convo.getAvatar(); - assert.property(avatar, 'content'); - }); - describe('when set to private', () => { it('correctly validates hex numbers', () => { const regularId = new Whisper.Conversation({ diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index 8c8226c2f..6fdbbcb0a 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -91,9 +91,10 @@ export class Avatar extends React.PureComponent { } public render() { - const { avatarPath, size } = this.props; + const { avatarPath, size, memberAvatars } = this.props; const { imageBroken } = this.state; - const hasImage = avatarPath && !imageBroken; + const isClosedGroupAvatar = memberAvatars && memberAvatars.length; + const hasImage = avatarPath && !imageBroken && !isClosedGroupAvatar; if ( size !== 28 && diff --git a/ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx b/ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx index 12d89d1be..431065f88 100644 --- a/ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx +++ b/ts/components/AvatarPlaceHolder/AvatarPlaceHolder.tsx @@ -46,8 +46,10 @@ export class AvatarPlaceHolder extends React.PureComponent { public render() { const { borderColor, colors, diameter, name } = this.props; + const diameterWithoutBorder = diameter - 2; const viewBox = `0 0 ${diameter} ${diameter}`; const r = diameter / 2; + const rWithoutBorder = diameterWithoutBorder / 2; if (!this.state.sha512Seed) { // return grey circle @@ -57,7 +59,7 @@ export class AvatarPlaceHolder extends React.PureComponent { { { const triggerId = `conversation-item-${phoneNumber}-ctxmenu`; return ( -
+
{ public renderAvatar() { const { from } = this.props; - const userName = from.phoneNumber || from.profileName; + const userName = from.profileName || from.phoneNumber; return ( { `module-message--${direction}`, expiring ? 'module-message--expired' : null )} - role="button" - onClick={event => { - const selection = window.getSelection(); - // Text is being selected - if (selection && selection.type === 'Range') { - return; - } - - // User clicked on message body - const target = event.target as HTMLDivElement; - if (target.className === 'text-selectable') { - return; - } - - this.props.onSelectMessage(); - }} > {this.renderError(isIncoming)} {isRss || isKickedFromGroup @@ -1138,6 +1122,22 @@ export class Message extends React.PureComponent { style={{ width: isShowingImage ? width : undefined, }} + role="button" + onClick={event => { + const selection = window.getSelection(); + // Text is being selected + if (selection && selection.type === 'Range') { + return; + } + + // User clicked on message body + const target = event.target as HTMLDivElement; + if (target.className === 'text-selectable') { + return; + } + + this.props.onSelectMessage(); + }} > {this.renderAuthor()} {this.renderQuote()} diff --git a/ts/components/session/usingClosedConversationDetails.tsx b/ts/components/session/usingClosedConversationDetails.tsx index e09f84ecf..ccac5fb4d 100644 --- a/ts/components/session/usingClosedConversationDetails.tsx +++ b/ts/components/session/usingClosedConversationDetails.tsx @@ -27,7 +27,7 @@ export function usingClosedConversationDetails(WrappedComponent: any) { void this.fetchClosedConversationDetails(); } - public componentDidUpdate() { + public componentWillReceiveProps() { void this.fetchClosedConversationDetails(); } @@ -66,7 +66,7 @@ export function usingClosedConversationDetails(WrappedComponent: any) { members.push(ourPrimary); } // no need to forward more than 2 conversations for rendering the group avatar - members.slice(0, 2); + members = members.slice(0, 2); const memberConvos = await Promise.all( members.map(async m => window.ConversationController.getOrCreateAndWait(m.key, 'private') @@ -79,10 +79,9 @@ export function usingClosedConversationDetails(WrappedComponent: any) { name: m.get('name') || m.get('profileName') || m.id, }; }); - - if (!_.isEqual(memberAvatars, this.state.memberAvatars)) { - this.setState({ memberAvatars }); - } + this.setState({ memberAvatars }); + } else { + this.setState({ memberAvatars: undefined }); } } }; diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 7c125267c..77780f73a 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -21,15 +21,19 @@ import { StringUtils } from '../session/utils'; import { UserUtil } from '../util'; export async function handleContentMessage(envelope: EnvelopePlus) { - const plaintext = await decrypt(envelope, envelope.content); + try { + const plaintext = await decrypt(envelope, envelope.content); - if (!plaintext) { - window.log.warn('handleContentMessage: plaintext was falsey'); - return; - } else if (plaintext instanceof ArrayBuffer && plaintext.byteLength === 0) { - return; + if (!plaintext) { + window.log.warn('handleContentMessage: plaintext was falsey'); + return; + } else if (plaintext instanceof ArrayBuffer && plaintext.byteLength === 0) { + return; + } + await innerHandleContentMessage(envelope, plaintext); + } catch (e) { + window.log.warn(e); } - await innerHandleContentMessage(envelope, plaintext); } async function decryptForMediumGroup( @@ -76,7 +80,7 @@ async function decryptForMediumGroup( window.console.info( 'Dropping message from ourself after decryptForMediumGroup' ); - return; + return null; } const plaintext = await decryptWithSenderKey( @@ -86,7 +90,7 @@ async function decryptForMediumGroup( sourceAsStr ); - return plaintext; + return unpad(plaintext); } function unpad(paddedData: ArrayBuffer): ArrayBuffer { @@ -388,81 +392,83 @@ export async function innerHandleContentMessage( plaintext: ArrayBuffer ): Promise { const { ConversationController } = window; - - const content = SignalService.Content.decode(new Uint8Array(plaintext)); - - const blocked = await isBlocked(envelope.source); - if (blocked) { - // We want to allow a blocked user message if that's a control message for a known group and the group is not blocked - if (shouldDropBlockedUserMessage(content)) { - window.log.info('Dropping blocked user message'); - return; - } else { - window.log.info('Allowing group-control message only from blocked user'); + try { + const content = SignalService.Content.decode(new Uint8Array(plaintext)); + + const blocked = await isBlocked(envelope.source); + if (blocked) { + // We want to allow a blocked user message if that's a control message for a known group and the group is not blocked + if (shouldDropBlockedUserMessage(content)) { + window.log.info('Dropping blocked user message'); + return; + } else { + window.log.info( + 'Allowing group-control message only from blocked user' + ); + } } - } - - const { FALLBACK_MESSAGE } = SignalService.Envelope.Type; + const { FALLBACK_MESSAGE } = SignalService.Envelope.Type; - await ConversationController.getOrCreateAndWait(envelope.source, 'private'); + await ConversationController.getOrCreateAndWait(envelope.source, 'private'); - if (content.preKeyBundleMessage) { - await handleSessionRequestMessage(envelope, content.preKeyBundleMessage); - } else if (envelope.type !== FALLBACK_MESSAGE) { - const device = new PubKey(envelope.source); + if (content.preKeyBundleMessage) { + await handleSessionRequestMessage(envelope, content.preKeyBundleMessage); + } else if (envelope.type !== FALLBACK_MESSAGE) { + const device = new PubKey(envelope.source); - await SessionProtocol.onSessionEstablished(device); - await libsession.getMessageQueue().processPending(device); - } + await SessionProtocol.onSessionEstablished(device); + await libsession.getMessageQueue().processPending(device); + } - if (content.pairingAuthorisation) { - await handlePairingAuthorisationMessage( - envelope, - content.pairingAuthorisation, - content.dataMessage - ); - return; - } + if (content.pairingAuthorisation) { + await handlePairingAuthorisationMessage( + envelope, + content.pairingAuthorisation, + content.dataMessage + ); + return; + } - if (content.syncMessage) { - await handleSyncMessage(envelope, content.syncMessage); - return; - } + if (content.syncMessage) { + await handleSyncMessage(envelope, content.syncMessage); + return; + } - if (content.dataMessage) { - if ( - content.dataMessage.profileKey && - content.dataMessage.profileKey.length === 0 - ) { - content.dataMessage.profileKey = null; + if (content.dataMessage) { + if ( + content.dataMessage.profileKey && + content.dataMessage.profileKey.length === 0 + ) { + content.dataMessage.profileKey = null; + } + await handleDataMessage(envelope, content.dataMessage); + return; } - await handleDataMessage(envelope, content.dataMessage); - return; - } - if (content.nullMessage) { - await handleNullMessage(envelope); - return; - } - if (content.callMessage) { - await handleCallMessage(envelope); - return; - } - if (content.receiptMessage) { - await handleReceiptMessage(envelope, content.receiptMessage); - return; - } - if (content.typingMessage) { - if ( - content.typingMessage.groupId && - content.typingMessage.groupId.length === 0 - ) { - content.typingMessage.groupId = null; + if (content.nullMessage) { + await handleNullMessage(envelope); + return; } - await handleTypingMessage(envelope, content.typingMessage); - return; + if (content.callMessage) { + await handleCallMessage(envelope); + return; + } + if (content.receiptMessage) { + await handleReceiptMessage(envelope, content.receiptMessage); + return; + } + if (content.typingMessage) { + if ( + content.typingMessage.groupId && + content.typingMessage.groupId.length === 0 + ) { + content.typingMessage.groupId = null; + } + await handleTypingMessage(envelope, content.typingMessage); + return; + } + } catch (e) { + window.log.warn(e); } - - return; } function onReadReceipt(readAt: any, timestamp: any, reader: any) { diff --git a/ts/session/crypto/MessageEncrypter.ts b/ts/session/crypto/MessageEncrypter.ts index 3380cb205..a605034cb 100644 --- a/ts/session/crypto/MessageEncrypter.ts +++ b/ts/session/crypto/MessageEncrypter.ts @@ -52,7 +52,7 @@ export async function encrypt( const plainText = padPlainTextBuffer(plainTextBuffer); if (encryptionType === EncryptionType.MediumGroup) { - return encryptForMediumGroup(device, plainTextBuffer); + return encryptForMediumGroup(device, plainText); } const address = new window.libsignal.SignalProtocolAddress(device.key, 1); diff --git a/ts/session/messages/outgoing/content/data/ChatMessage.ts b/ts/session/messages/outgoing/content/data/ChatMessage.ts index 3f07a3de4..83026306c 100644 --- a/ts/session/messages/outgoing/content/data/ChatMessage.ts +++ b/ts/session/messages/outgoing/content/data/ChatMessage.ts @@ -108,7 +108,7 @@ export class ChatMessage extends DataMessage { } dataMessage.profile = profile; } - if (this.profileKey) { + if (this.profileKey && this.profileKey.length) { dataMessage.profileKey = this.profileKey; } diff --git a/ts/session/messages/outgoing/content/data/ExpirationTimerUpdateMessage.ts b/ts/session/messages/outgoing/content/data/ExpirationTimerUpdateMessage.ts index 3a25caa7b..2da6fc6b3 100644 --- a/ts/session/messages/outgoing/content/data/ExpirationTimerUpdateMessage.ts +++ b/ts/session/messages/outgoing/content/data/ExpirationTimerUpdateMessage.ts @@ -48,7 +48,7 @@ export class ExpirationTimerUpdateMessage extends DataMessage { if (this.expireTimer) { data.expireTimer = this.expireTimer; } - if (this.profileKey) { + if (this.profileKey && this.profileKey.length) { data.profileKey = this.profileKey; } diff --git a/ts/session/messages/outgoing/content/data/group/ClosedGroupMessage.ts b/ts/session/messages/outgoing/content/data/group/ClosedGroupMessage.ts index 9b6c80704..c17fdbe4f 100644 --- a/ts/session/messages/outgoing/content/data/group/ClosedGroupMessage.ts +++ b/ts/session/messages/outgoing/content/data/group/ClosedGroupMessage.ts @@ -32,7 +32,12 @@ export abstract class ClosedGroupMessage extends DataMessage { } protected groupContext(): SignalService.GroupContext { - const id = new Uint8Array(StringUtils.encode(this.groupId.key, 'utf8')); + let groupIdWithPrefix: string = this.groupId.key; + if (!this.groupId.key.startsWith(PubKey.PREFIX_GROUP_TEXTSECURE)) { + groupIdWithPrefix = PubKey.PREFIX_GROUP_TEXTSECURE + this.groupId.key; + } + const encoded = StringUtils.encode(groupIdWithPrefix, 'utf8'); + const id = new Uint8Array(encoded); return new SignalService.GroupContext({ id }); } diff --git a/ts/session/types/PubKey.ts b/ts/session/types/PubKey.ts index d3fbb7c0c..7e63b32e9 100644 --- a/ts/session/types/PubKey.ts +++ b/ts/session/types/PubKey.ts @@ -1,17 +1,29 @@ export class PubKey { public static readonly PUBKEY_LEN = 66; + private static readonly HEX = '[0-9a-fA-F]'; + // This is a temporary fix to allow groupPubkeys created from mobile to be handled correctly // They have a different regex to match // FIXME move this to a new class which validates group ids and use it in all places where we have group ids (message sending included) - public static readonly MOBILE_GROUP_PUBKEY_LEN = 32; - public static readonly regexForPubkeys = `((05)?[0-9a-fA-F]{${PubKey.PUBKEY_LEN - - 2}})`; - - private static readonly regexForMobileGroupID = `__textsecure_group__![0-9a-fA-F]{${PubKey.MOBILE_GROUP_PUBKEY_LEN}}`; + // tslint:disable: member-ordering + public static readonly regexForPubkeys = `((05)?${PubKey.HEX}{64})`; + public static readonly PREFIX_GROUP_TEXTSECURE = '__textsecure_group__!'; // prettier-ignore private static readonly regex: RegExp = new RegExp( - `^${PubKey.regexForPubkeys}|${PubKey.regexForMobileGroupID}$` + `^(${PubKey.PREFIX_GROUP_TEXTSECURE})?(05)?(${PubKey.HEX}{64}|${PubKey.HEX}{32})$` ); + /** + * If you want to update this regex. Be sure that those are matches ; + * __textsecure_group__!05010203040506070809a0b0c0d0e0f0ff010203040506070809a0b0c0d0e0f0ff + * __textsecure_group__!010203040506070809a0b0c0d0e0f0ff010203040506070809a0b0c0d0e0f0ff + * __textsecure_group__!05010203040506070809a0b0c0d0e0f0ff + * __textsecure_group__!010203040506070809a0b0c0d0e0f0ff + * 05010203040506070809a0b0c0d0e0f0ff010203040506070809a0b0c0d0e0f0ff + * 010203040506070809a0b0c0d0e0f0ff010203040506070809a0B0c0d0e0f0FF + * 05010203040506070809a0b0c0d0e0f0ff + * 010203040506070809a0b0c0d0e0f0ff + */ + public readonly key: string; /** diff --git a/ts/test/session/unit/messages/ClosedGroupChatMessage_test.ts b/ts/test/session/unit/messages/ClosedGroupChatMessage_test.ts index 74692fd58..beac186eb 100644 --- a/ts/test/session/unit/messages/ClosedGroupChatMessage_test.ts +++ b/ts/test/session/unit/messages/ClosedGroupChatMessage_test.ts @@ -31,7 +31,12 @@ describe('ClosedGroupChatMessage', () => { .to.have.property('group') .to.have.deep.property( 'id', - new Uint8Array(StringUtils.encode(groupId.key, 'utf8')) + new Uint8Array( + StringUtils.encode( + PubKey.PREFIX_GROUP_TEXTSECURE + groupId.key, + 'utf8' + ) + ) ); expect(decoded.dataMessage) .to.have.property('group') diff --git a/ts/util/getInitials.ts b/ts/util/getInitials.ts index 8f7e30b5e..dd0d62464 100644 --- a/ts/util/getInitials.ts +++ b/ts/util/getInitials.ts @@ -1,21 +1,11 @@ -const BAD_CHARACTERS = /[^A-Za-z\s]+/g; -const WHITESPACE = /\s+/g; - -function removeNonInitials(name: string) { - return name.replace(BAD_CHARACTERS, '').replace(WHITESPACE, ' '); -} - export function getInitials(name?: string): string | undefined { - if (!name) { + if (!name || !name.length) { return; } - const cleaned = removeNonInitials(name); - const parts = cleaned.split(' '); - const initials = parts.map(part => part.trim()[0]); - if (!initials.length) { - return; + if (name.length > 2 && name.startsWith('05')) { + return name[2]; } - return initials[0]; + return name[0]; }