From 27f205e0a374db39d7f59ef8dd99a783ef6e3a2d Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 9 Apr 2018 20:02:56 -0400 Subject: [PATCH 01/16] Fix TS error regarding `SharedArrayBuffer` --- tsconfig.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 37eb7b9c0..d056caa08 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,9 @@ /* Basic Options */ "target": "es2016", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - // "lib": [], /* Specify library files to be included in the compilation. */ + "lib": [ /* Specify library files to be included in the compilation. */ + "es2017", // Required by `@sindresorhus/is` + ], // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ From bcd3e26377d386f74e32a5e399659e02b359857c Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 9 Apr 2018 20:10:52 -0400 Subject: [PATCH 02/16] Fix TS warning about `appendChild` --- ts/components/utility/BackboneWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/components/utility/BackboneWrapper.tsx b/ts/components/utility/BackboneWrapper.tsx index b66e22685..a57651ced 100644 --- a/ts/components/utility/BackboneWrapper.tsx +++ b/ts/components/utility/BackboneWrapper.tsx @@ -22,7 +22,7 @@ interface BackboneViewConstructor { * while we slowly replace the internals of a given Backbone view with React. */ export class BackboneWrapper extends React.Component { - protected el: Element | null = null; + protected el: HTMLElement | null = null; protected view: BackboneView | null = null; public componentWillUnmount() { From 9513e90a84d4527b0a8434e0b5c73886fa85566a Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 11 Apr 2018 11:20:11 -0400 Subject: [PATCH 03/16] Fix TS error regarding `window` --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.json b/tsconfig.json index d056caa08..ef472b287 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "target": "es2016", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ "lib": [ /* Specify library files to be included in the compilation. */ + "dom", // Required to access `window` "es2017", // Required by `@sindresorhus/is` ], // "allowJs": true, /* Allow javascript files to be compiled. */ From f25a579f32c0878d82973da0cea6c5e93e572819 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 9 Apr 2018 19:26:48 -0400 Subject: [PATCH 04/16] Add basic `Message` type definition --- ts/types/Message.ts | 47 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 ts/types/Message.ts diff --git a/ts/types/Message.ts b/ts/types/Message.ts new file mode 100644 index 000000000..d41f59f1b --- /dev/null +++ b/ts/types/Message.ts @@ -0,0 +1,47 @@ +import { Attachment } from './Attachment'; + + +export type Message = IncomingMessage | OutgoingMessage; + +export type IncomingMessage = { + type: 'incoming'; + attachments: Array; + body?: string; + conversationId: string; + decrypted_at?: number; + errors?: Array; + flags?: number; + id: string; + received_at: number; + sent_at: number; + source?: string; + sourceDevice?: number; + timestamp: number; +} & Message4 + +export type OutgoingMessage = { + type: 'outgoing'; + attachments: Array; + body?: string; + conversationId: string; + delivered: number; + delivered_to: Array; + destination: string; // PhoneNumber + expirationStartTimestamp: number; + expires_at?: number; + expireTimer?: number; + id: string; + received_at: number; + recipients?: Array; // Array + sent: boolean; + sent_at: number; + sent_to: Array; // Array + synced: boolean; + timestamp: number; +} & Message4 + +interface Message4 { + numAttachments?: number; + numVisualMediaAttachments?: number; + numFileAttachments?: number; +} From fa36e1b7a7a39d2cfbb80070f1949d7d0a2410ac Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 11 Apr 2018 18:03:30 -0400 Subject: [PATCH 05/16] Configure EditorConfig for TypeScript --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 5bfccc902..4adf083b3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,5 +10,5 @@ indent_style = space insert_final_newline = true trim_trailing_whitespace = true -[{js/modules/**/*.js, test/app/**/*.js, test/modules/**/*.js}] +[{js/modules/**/*.js, ts/**/*.ts, test/app/**/*.js, test/modules/**/*.js}] indent_size = 2 From 65bf34d1b87e9c9f54b885f93c9271b1fce69a36 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Mon, 9 Apr 2018 19:28:54 -0400 Subject: [PATCH 06/16] Add basic `Attachment` type definition --- ts/types/Attachment.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 ts/types/Attachment.ts diff --git a/ts/types/Attachment.ts b/ts/types/Attachment.ts new file mode 100644 index 000000000..68dc4e145 --- /dev/null +++ b/ts/types/Attachment.ts @@ -0,0 +1,21 @@ +import is from '@sindresorhus/is'; + +import { MIMEType } from './MIME'; + + +export interface Attachment { + fileName?: string; + contentType?: MIMEType; + size?: number; + data: ArrayBuffer; + + // // Omit unused / deprecated keys: + // schemaVersion?: number; + // id?: string; + // width?: number; + // height?: number; + // thumbnail?: ArrayBuffer; + // key?: ArrayBuffer; + // digest?: ArrayBuffer; + // flags?: number; +} From b50c55172ddccdedb34f3cd168124448933ca9f4 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 11 Apr 2018 18:35:24 -0400 Subject: [PATCH 07/16] Add `MIME` type --- ts/types/MIME.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 ts/types/MIME.ts diff --git a/ts/types/MIME.ts b/ts/types/MIME.ts new file mode 100644 index 000000000..ade12d7d5 --- /dev/null +++ b/ts/types/MIME.ts @@ -0,0 +1 @@ +export type MIMEType = string & { _mimeTypeBrand: any }; From 1659354f5124ab7bfc73d3f456107b176c7538f0 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 11 Apr 2018 18:36:11 -0400 Subject: [PATCH 08/16] Expand `Message` type definitions --- ts/types/Message.ts | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/ts/types/Message.ts b/ts/types/Message.ts index d41f59f1b..f539ed1a8 100644 --- a/ts/types/Message.ts +++ b/ts/types/Message.ts @@ -1,29 +1,28 @@ import { Attachment } from './Attachment'; -export type Message = IncomingMessage | OutgoingMessage; +export type Message + = IncomingMessage + | OutgoingMessage + | VerifiedChangeMessage; -export type IncomingMessage = { +export type IncomingMessage = Readonly<{ type: 'incoming'; attachments: Array; body?: string; - conversationId: string; decrypted_at?: number; errors?: Array; flags?: number; id: string; received_at: number; - sent_at: number; source?: string; sourceDevice?: number; - timestamp: number; -} & Message4 +} & SharedMessageProperties & Message4 & ExpirationTimerUpdate>; -export type OutgoingMessage = { +export type OutgoingMessage = Readonly<{ type: 'outgoing'; attachments: Array; body?: string; - conversationId: string; delivered: number; delivered_to: Array; destination: string; // PhoneNumber @@ -34,14 +33,30 @@ export type OutgoingMessage = { received_at: number; recipients?: Array; // Array sent: boolean; - sent_at: number; sent_to: Array; // Array synced: boolean; +} & SharedMessageProperties & Message4 & ExpirationTimerUpdate>; + +export type VerifiedChangeMessage = Readonly<{ + type: 'verified-change'; +} & SharedMessageProperties & Message4 & ExpirationTimerUpdate>; + +type SharedMessageProperties = Readonly<{ + conversationId: string; + sent_at: number; timestamp: number; -} & Message4 +}>; + +type ExpirationTimerUpdate = Readonly<{ + expirationTimerUpdate?: Readonly<{ + expireTimer: number; + fromSync: boolean; + source: string; // PhoneNumber + }>, +}>; -interface Message4 { +type Message4 = Readonly<{ numAttachments?: number; numVisualMediaAttachments?: number; numFileAttachments?: number; -} +}>; From cca5db323708532e7d3f34fe380b1f5d8938ca18 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 11 Apr 2018 18:49:14 -0400 Subject: [PATCH 09/16] Remove unused import --- ts/types/Attachment.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/ts/types/Attachment.ts b/ts/types/Attachment.ts index 68dc4e145..1e3d14e65 100644 --- a/ts/types/Attachment.ts +++ b/ts/types/Attachment.ts @@ -1,5 +1,3 @@ -import is from '@sindresorhus/is'; - import { MIMEType } from './MIME'; From 44debd123d98143b252b5bfd1ccfeb0f78e647b4 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 11 Apr 2018 18:49:46 -0400 Subject: [PATCH 10/16] Add basic implementation of `Conversation.updateFromLastMessage` --- ts/test/types/Conversation_test.ts | 102 +++++++++++++++++++++++++++++ ts/types/Conversation.ts | 45 +++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 ts/test/types/Conversation_test.ts create mode 100644 ts/types/Conversation.ts diff --git a/ts/test/types/Conversation_test.ts b/ts/test/types/Conversation_test.ts new file mode 100644 index 000000000..f5aea6fc6 --- /dev/null +++ b/ts/test/types/Conversation_test.ts @@ -0,0 +1,102 @@ +import 'mocha'; +import { assert } from 'chai'; + +import * as Conversation from '../../types/Conversation'; +import { + IncomingMessage, + OutgoingMessage, + VerifiedChangeMessage, +} from '../../types/Message'; + +describe('Conversation', () => { + describe('createLastMessageUpdate', () => { + it('should reset last message if conversation has no messages', () => { + const input = { + currentLastMessageText: null, + currentTimestamp: null, + lastMessage: null, + lastMessageNotificationText: null, + }; + const expected = { + lastMessage: '', + timestamp: null, + }; + + const actual = Conversation.createLastMessageUpdate(input); + assert.deepEqual(actual, expected); + }); + + context('for regular message', () => { + it('should update last message text and timestamp', () => { + const input = { + currentLastMessageText: 'Existing message', + currentTimestamp: 555, + lastMessage: { + type: 'outgoing', + conversationId: 'foo', + sent_at: 666, + timestamp: 666, + } as OutgoingMessage, + lastMessageNotificationText: 'New outgoing message', + }; + const expected = { + lastMessage: 'New outgoing message', + timestamp: 666, + }; + + const actual = Conversation.createLastMessageUpdate(input); + assert.deepEqual(actual, expected); + }); + }); + context('for verified change message', () => { + it('should skip update', () => { + const input = { + currentLastMessageText: 'bingo', + currentTimestamp: 555, + lastMessage: { + type: 'verified-change', + conversationId: 'foo', + sent_at: 666, + timestamp: 666, + } as VerifiedChangeMessage, + lastMessageNotificationText: 'Verified Changed', + }; + const expected = { + lastMessage: 'bingo', + timestamp: 555, + }; + + const actual = Conversation.createLastMessageUpdate(input); + assert.deepEqual(actual, expected); + }); + }); + + context('for expired message', () => { + it('should update message but not timestamp (to prevent bump to top)', () => { + const input = { + currentLastMessageText: 'I am expired', + currentTimestamp: 555, + lastMessage: { + type: 'incoming', + conversationId: 'foo', + sent_at: 666, + timestamp: 666, + expirationTimerUpdate: { + expireTimer: 111, + fromSync: false, + source: '+12223334455', + }, + } as IncomingMessage, + lastMessageNotificationText: 'Last message before expired', + }; + const expected = { + lastMessage: 'Last message before expired', + timestamp: 555, + }; + + const actual = Conversation.createLastMessageUpdate(input); + assert.deepEqual(actual, expected); + }); + }); + }); +}); diff --git a/ts/types/Conversation.ts b/ts/types/Conversation.ts new file mode 100644 index 000000000..e31b3b0a6 --- /dev/null +++ b/ts/types/Conversation.ts @@ -0,0 +1,45 @@ +import is from '@sindresorhus/is'; +import { Message } from './Message'; + + +interface ConversationLastMessageUpdate { + lastMessage: string | null; + timestamp: number | null; +} + +export const createLastMessageUpdate = ({ + currentLastMessageText, + currentTimestamp, + lastMessage, + lastMessageNotificationText, +}: { + currentLastMessageText: string | null, + currentTimestamp: number | null, + lastMessage: Message | null, + lastMessageNotificationText: string | null, +}): ConversationLastMessageUpdate => { + if (lastMessage === null) { + return { + lastMessage: '', + timestamp: null, + }; + } + + const { type } = lastMessage; + const isVerifiedChangeMessage = type === 'verified-change'; + const isExpiringMessage = is.object(lastMessage.expirationTimerUpdate); + const shouldUpdateTimestamp = !isVerifiedChangeMessage && !isExpiringMessage; + + const newTimestamp = shouldUpdateTimestamp ? + lastMessage.sent_at : + currentTimestamp; + + const shouldUpdateLastMessageText = !isVerifiedChangeMessage; + const newLastMessageText = shouldUpdateLastMessageText ? + lastMessageNotificationText : currentLastMessageText; + + return { + lastMessage: newLastMessageText, + timestamp: newTimestamp, + }; +}; From ad05efb7a088fd634488d62129811c90a0a51b61 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 11 Apr 2018 19:23:03 -0400 Subject: [PATCH 11/16] Expose `Signal.Types.Conversation` --- preload.js | 1 + test/styleguide/legacy_bridge.js | 1 + 2 files changed, 2 insertions(+) diff --git a/preload.js b/preload.js index aca6cbabc..a547ee69a 100644 --- a/preload.js +++ b/preload.js @@ -183,6 +183,7 @@ window.Signal.Startup = require('./js/modules/startup'); window.Signal.Types = {}; window.Signal.Types.Attachment = Attachment; +window.Signal.Types.Conversation = require('./ts/types/Conversation'); window.Signal.Types.Errors = require('./js/modules/types/errors'); window.Signal.Types.Message = Message; diff --git a/test/styleguide/legacy_bridge.js b/test/styleguide/legacy_bridge.js index 7f07ac287..5b2c58a97 100644 --- a/test/styleguide/legacy_bridge.js +++ b/test/styleguide/legacy_bridge.js @@ -34,6 +34,7 @@ window.Signal.Migrations.V17 = {}; window.Signal.OS = {}; window.Signal.Types = {}; window.Signal.Types.Attachment = {}; +window.Signal.Types.Conversation = {}; window.Signal.Types.Errors = {}; window.Signal.Types.Message = { initializeSchemaVersion: attributes => attributes, From 0902c9409348a02ce350c070ec6c60e185fa9bac Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 11 Apr 2018 19:23:30 -0400 Subject: [PATCH 12/16] Reset last message after message has expired Fixes #980. --- js/models/conversations.js | 45 ++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 72ff82394..63c5c92f4 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -9,7 +9,7 @@ 'use strict'; window.Whisper = window.Whisper || {}; - const { Attachment, Message } = window.Signal.Types; + const { Attachment, Conversation, Message } = window.Signal.Types; const { upgradeMessageSchema, loadAttachmentData } = window.Signal.Migrations; // TODO: Factor out private and group subclasses of Conversation @@ -669,31 +669,28 @@ )); }); }, - /* jshint ignore:end */ - /* eslint-disable */ - updateLastMessage: function() { - var collection = new Whisper.MessageCollection(); - return collection.fetchConversation(this.id, 1).then(function() { - var lastMessage = collection.at(0); - if (lastMessage) { - var type = lastMessage.get('type'); - var shouldSkipUpdate = type === 'verified-change' || lastMessage.get('expirationTimerUpdate'); - if (shouldSkipUpdate) { - return; - } - this.set({ - lastMessage : lastMessage.getNotificationText(), - timestamp : lastMessage.get('sent_at') - }); - } else { - this.set({ lastMessage: '', timestamp: null }); - } - if (this.hasChanged('lastMessage') || this.hasChanged('timestamp')) { - this.save(); - } - }.bind(this)); + async updateLastMessage() { + const collection = new Whisper.MessageCollection(); + await collection.fetchConversation(this.id, 1); + const lastMessage = collection.at(0); + + const lastMessageUpdate = Conversation.createLastMessageUpdate({ + currentLastMessageText: this.get('lastMessage') || null, + currentTimestamp: this.get('timestamp') || null, + lastMessage: lastMessage ? lastMessage.toJSON() : null, + lastMessageNotificationText: lastMessage + ? lastMessage.getNotificationText() : null, + }); + + this.set(lastMessageUpdate); + + if (this.hasChanged('lastMessage') || this.hasChanged('timestamp')) { + this.save(); + } }, + /* jshint ignore:end */ + /* eslint-disable */ updateExpirationTimer: function(expireTimer, source, received_at, options) { options = options || {}; From 9d159da79c9ab248672ad0413292ba1f5120d7f7 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 11 Apr 2018 19:45:31 -0400 Subject: [PATCH 13/16] Add Microsoft contributed TSlint rules: `tslint-microsoft-contrib` --- package.json | 1 + yarn.lock | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/package.json b/package.json index 967fd0171..66fa39e8d 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,7 @@ "spectron": "^3.8.0", "ts-loader": "^4.1.0", "tslint": "^5.9.1", + "tslint-microsoft-contrib": "^5.0.3", "tslint-react": "^3.5.1", "typescript": "^2.8.1", "webpack": "^4.4.1" diff --git a/yarn.lock b/yarn.lock index d957d4cf9..9a9c20ed8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8866,6 +8866,12 @@ tslib@^1.8.0, tslib@^1.8.1: version "1.9.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8" +tslint-microsoft-contrib@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/tslint-microsoft-contrib/-/tslint-microsoft-contrib-5.0.3.tgz#6fc3e238179cd72045c2b422e4d655f4183a8d5c" + dependencies: + tsutils "^2.12.1" + tslint-react@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/tslint-react/-/tslint-react-3.5.1.tgz#a5ca48034bf583fb63b42763bb89fa23062d5390" From 0daccee2093469d959179fbaf20b420ae9310540 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 11 Apr 2018 19:45:48 -0400 Subject: [PATCH 14/16] Enable TSLint Mocha rules --- tslint.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tslint.json b/tslint.json index 060e9c463..95ebd6f78 100644 --- a/tslint.json +++ b/tslint.json @@ -8,6 +8,13 @@ "rules": { "array-type": [true, "generic"], "interface-name": [true, "never-prefix"], + + "mocha-avoid-only": true, + // Disabled until we can allow dynamically generated tests: + // https://github.com/Microsoft/tslint-microsoft-contrib/issues/85#issuecomment-371749352 + "mocha-no-side-effect-code": false, + "mocha-unneeded-done": true, + "no-consecutive-blank-lines": [true, 2], "object-literal-key-quotes": [true, "as-needed"], "object-literal-sort-keys": false, @@ -22,5 +29,7 @@ "quotemark": [true, "single", "jsx-double", "avoid-template", "avoid-escape"] }, - "rulesDirectory": [] + "rulesDirectory": [ + "node_modules/tslint-microsoft-contrib" + ] } From 560cc4e1498aacaee2c7f5fc913d1691b10a583f Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Wed, 11 Apr 2018 19:46:21 -0400 Subject: [PATCH 15/16] Set dynamic parameter last --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 66fa39e8d..8a5396ece 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "jscs": "yarn grunt jscs", "jshint": "yarn grunt jshint", "lint": "yarn eslint && yarn grunt lint && yarn tslint", - "tslint": "tslint --config tslint.json --project . --format stylish", + "tslint": "tslint --config tslint.json --format stylish --project .", "transpile": "tsc", "clean-transpile": "rimraf ts/**/*.js ts/*.js", "open-coverage": "open coverage/lcov-report/index.html", From 5f8148d3dab2d2b42ba9373798f73a4aa448b4e7 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Fri, 13 Apr 2018 14:26:01 -0400 Subject: [PATCH 16/16] Bind to `Conversation` lazily to prevent style guide errors --- js/models/conversations.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 63c5c92f4..e2bcc7d60 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -9,7 +9,7 @@ 'use strict'; window.Whisper = window.Whisper || {}; - const { Attachment, Conversation, Message } = window.Signal.Types; + const { Attachment, Message } = window.Signal.Types; const { upgradeMessageSchema, loadAttachmentData } = window.Signal.Migrations; // TODO: Factor out private and group subclasses of Conversation @@ -675,7 +675,7 @@ await collection.fetchConversation(this.id, 1); const lastMessage = collection.at(0); - const lastMessageUpdate = Conversation.createLastMessageUpdate({ + const lastMessageUpdate = Signal.Types.Conversation.createLastMessageUpdate({ currentLastMessageText: this.get('lastMessage') || null, currentTimestamp: this.get('timestamp') || null, lastMessage: lastMessage ? lastMessage.toJSON() : null,