From 2a48d3c9bc83e9083c3a9d7366c008f3909c9f6d Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 07:40:39 +0100 Subject: [PATCH 001/115] First pass at a PosthogAnalytics class --- package.json | 1 + src/PosthogAnalytics.ts | 82 ++++++++++++++++++++++++++++++++++ test/PosthogAnalytics-test.ts | 84 +++++++++++++++++++++++++++++++++++ yarn.lock | 12 +++++ 4 files changed, 179 insertions(+) create mode 100644 src/PosthogAnalytics.ts create mode 100644 test/PosthogAnalytics-test.ts diff --git a/package.json b/package.json index e80ed8dd5a..805531abff 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "pako": "^2.0.3", "parse5": "^6.0.1", "png-chunks-extract": "^1.0.0", + "posthog-js": "^1.12.1", "prop-types": "^15.7.2", "qrcode": "^1.4.4", "re-resizable": "^6.9.0", diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts new file mode 100644 index 0000000000..1ca2d37de7 --- /dev/null +++ b/src/PosthogAnalytics.ts @@ -0,0 +1,82 @@ +import posthog from 'posthog-js'; +import SdkConfig from './SdkConfig'; + +export interface IEvent { + key: string; + properties: {} +} + +export interface IOnboardingLoginBegin extends IEvent { + key: "onboarding_login_begin", +} + +const hashHex = async (input: string): Promise => { + const buf = new TextEncoder().encode(input); + const digestBuf = await window.crypto.subtle.digest("sha-256", buf); + return [...new Uint8Array(digestBuf)].map((b: number) => b.toString(16).padStart(2, "0")).join(""); +}; + +export class PosthogAnalytics { + private onlyTrackAnonymousEvents = false; + private initialised = false; + private posthog = null; + + private static _instance = null; + + public static instance(): PosthogAnalytics { + if (!this.instance) { + this._instance = new PosthogAnalytics(posthog); + } + return this._instance; + } + + constructor(posthog) { + this.posthog = posthog; + } + + public init(onlyTrackAnonymousEvents: boolean) { + if (Boolean(navigator.doNotTrack === "1")) { + this.initialised = false; + return; + } + this.onlyTrackAnonymousEvents = onlyTrackAnonymousEvents; + const posthogConfig = SdkConfig.get()["posthog"]; + if (posthogConfig) { + console.log(`Initialising Posthog for ${posthogConfig.apiHost}`); + this.posthog.init(posthogConfig.projectApiKey, { api_host: posthogConfig.apiHost }); + this.initialised = true; + } + } + + public isInitialised(): boolean { + return this.initialised; + } + + public setOnlyTrackAnonymousEvents(enabled: boolean) { + this.onlyTrackAnonymousEvents = enabled; + } + + public track( + key: E["key"], + properties: E["properties"], + anonymous = false, + ) { + if (!this.initialised) return; + if (this.onlyTrackAnonymousEvents && !anonymous) return; + + this.posthog.capture(key, properties); + } + + public async trackRoomEvent( + key: E["key"], + roomId: string, + properties: E["properties"], + ...args + ) { + const updatedProperties = { + ...properties, + hashedRoomId: roomId ? await hashHex(roomId) : null, + }; + this.track(key, updatedProperties, ...args); + } +} diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts new file mode 100644 index 0000000000..a37d5cb2c8 --- /dev/null +++ b/test/PosthogAnalytics-test.ts @@ -0,0 +1,84 @@ +import { IEvent, PosthogAnalytics } from '../src/PosthogAnalytics'; +import SdkConfig from '../src/SdkConfig'; +const crypto = require('crypto'); + +class FakePosthog { + public capture; + public init; + + constructor() { + this.capture = jest.fn(); + this.init = jest.fn(); + } +} + +export interface ITestEvent extends IEvent { + key: "jest_test_event", + properties: { + foo: string + } +} + +describe("PosthogAnalytics", () => { + let analytics: PosthogAnalytics; + let fakePosthog: FakePosthog; + + beforeEach(() => { + fakePosthog = new FakePosthog(); + analytics = new PosthogAnalytics(fakePosthog); + window.crypto = { + subtle: crypto.webcrypto.subtle, + }; + }); + + afterEach(() => { + navigator.doNotTrack = null; + window.crypto = null; + }); + + it("Should not initialise if DNT is enabled", () => { + navigator.doNotTrack = "1"; + analytics.init(false); + expect(analytics.isInitialised()).toBe(false); + }); + + it("Should not initialise if config is not set", () => { + jest.spyOn(SdkConfig, "get").mockReturnValue({}); + analytics.init(false); + expect(analytics.isInitialised()).toBe(false); + }); + + it("Should initialise if config is set", () => { + jest.spyOn(SdkConfig, "get").mockReturnValue({ + posthog: { + projectApiKey: "foo", + apiHost: "bar", + }, + }); + analytics.init(false); + expect(analytics.isInitialised()).toBe(true); + }); + + it("Should pass track() to posthog", () => { + analytics.init(false); + analytics.track("jest_test_event", { + foo: "bar", + }); + expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); + expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ foo: "bar" }); + }); + + it("Should pass trackRoomEvent to posthog", () => { + analytics.init(false); + const roomId = "42"; + return analytics.trackRoomEvent("jest_test_event", roomId, { + foo: "bar", + }).then(() => { + expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); + expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ + foo: "bar", + hashedRoomId: "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049", + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 96c02681fd..9d41c37b12 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3601,6 +3601,11 @@ fbjs@^0.8.4: setimmediate "^1.0.5" ua-parser-js "^0.7.18" +fflate@^0.4.1: + version "0.4.8" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" + integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== + file-entry-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.0.tgz#7921a89c391c6d93efec2169ac6bf300c527ea0a" @@ -6287,6 +6292,13 @@ postcss@^8.0.2: nanoid "^3.1.20" source-map "^0.6.1" +posthog-js@^1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.12.1.tgz#97834ee2574f34ffb5db2f5b07452c847e3c4d27" + integrity sha512-Y3lzcWkS8xFY6Ryj3I4ees7qWP2WGkLw0Arcbk5xaT0+5YlA6UC2jlL/+fN9bz/Bl62EoN3BML901Cuot/QNjg== + dependencies: + fflate "^0.4.1" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" From d4550c1a28a61a8026590f83ebea5c6589406a8f Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 07:42:44 +0100 Subject: [PATCH 002/115] Remove console logging --- src/PosthogAnalytics.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 1ca2d37de7..5b2a601adc 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -42,7 +42,6 @@ export class PosthogAnalytics { this.onlyTrackAnonymousEvents = onlyTrackAnonymousEvents; const posthogConfig = SdkConfig.get()["posthog"]; if (posthogConfig) { - console.log(`Initialising Posthog for ${posthogConfig.apiHost}`); this.posthog.init(posthogConfig.projectApiKey, { api_host: posthogConfig.apiHost }); this.initialised = true; } From 3135e425865232bfd1e1b131a97ec067c150802d Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 07:42:55 +0100 Subject: [PATCH 003/115] Add test for silently ignoring messages when not initialised --- test/PosthogAnalytics-test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index a37d5cb2c8..56e6af8666 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -81,4 +81,12 @@ describe("PosthogAnalytics", () => { }); }); }); + + it("Should silently not send messages if not inititalised", () => { + analytics.track("jest_test_event", { + foo: "bar", + }); + + expect(fakePosthog.capture.mock.calls.length).toBe(0); + }); }); From 74b0e52f9a2ac243af50ab419da9c296b74a1540 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 08:23:42 +0100 Subject: [PATCH 004/115] Enforce anon/pseudo-anon via types --- src/PosthogAnalytics.ts | 48 +++++++++++++++++++++++++---------- test/PosthogAnalytics-test.ts | 25 +++++++++++++----- 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 5b2a601adc..133c9275d4 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -1,11 +1,28 @@ import posthog from 'posthog-js'; import SdkConfig from './SdkConfig'; -export interface IEvent { - key: string; +interface IEvent { + // The event name that will be used by PostHog. + // TODO: standard format (camel case? snake? UpperCase?) + eventName: string; + + // The properties of the event that will be stored in PostHog. properties: {} } +// If an event extends IPseudonymousEvent, the event contains pseudonymous data +// that won't be sent unless the user has explicitly consented to pseudonymous tracking. +// For example, hashed user IDs or room IDs. +export interface IPseudonymousEvent extends IEvent {} + +// If an event extends IAnonymousEvent, the event strictly contains *only* anonymous data which +// may be sent without explicit user consent. +export interface IAnonymousEvent extends IEvent {} + +export interface IRoomEvent extends IPseudonymousEvent { + hashedRoomId: string +} + export interface IOnboardingLoginBegin extends IEvent { key: "onboarding_login_begin", } @@ -55,27 +72,32 @@ export class PosthogAnalytics { this.onlyTrackAnonymousEvents = enabled; } - public track( - key: E["key"], + public trackPseudonymousEvent( + eventName: E["eventName"], properties: E["properties"], - anonymous = false, ) { if (!this.initialised) return; - if (this.onlyTrackAnonymousEvents && !anonymous) return; - - this.posthog.capture(key, properties); + if (this.onlyTrackAnonymousEvents) return; + this.posthog.capture(eventName, properties); } - public async trackRoomEvent( - key: E["key"], - roomId: string, + public trackAnonymousEvent( + eventName: E["eventName"], properties: E["properties"], - ...args + ) { + if (!this.initialised) return; + this.posthog.capture(eventName, properties); + } + + public async trackRoomEvent( + eventName: E["eventName"], + roomId: string, + properties: Omit, ) { const updatedProperties = { ...properties, hashedRoomId: roomId ? await hashHex(roomId) : null, }; - this.track(key, updatedProperties, ...args); + this.trackPseudonymousEvent(eventName, updatedProperties); } } diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index 56e6af8666..dfadac921d 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -1,4 +1,4 @@ -import { IEvent, PosthogAnalytics } from '../src/PosthogAnalytics'; +import { IAnonymousEvent, IRoomEvent, PosthogAnalytics } from '../src/PosthogAnalytics'; import SdkConfig from '../src/SdkConfig'; const crypto = require('crypto'); @@ -12,13 +12,20 @@ class FakePosthog { } } -export interface ITestEvent extends IEvent { +export interface ITestEvent extends IAnonymousEvent { key: "jest_test_event", properties: { foo: string } } +export interface ITestRoomEvent extends IRoomEvent { + key: "jest_test_room_event", + properties: { + foo: string + } +} + describe("PosthogAnalytics", () => { let analytics: PosthogAnalytics; let fakePosthog: FakePosthog; @@ -61,7 +68,7 @@ describe("PosthogAnalytics", () => { it("Should pass track() to posthog", () => { analytics.init(false); - analytics.track("jest_test_event", { + analytics.trackAnonymousEvent("jest_test_event", { foo: "bar", }); expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); @@ -71,7 +78,7 @@ describe("PosthogAnalytics", () => { it("Should pass trackRoomEvent to posthog", () => { analytics.init(false); const roomId = "42"; - return analytics.trackRoomEvent("jest_test_event", roomId, { + return analytics.trackRoomEvent("jest_test_event", roomId, { foo: "bar", }).then(() => { expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); @@ -82,11 +89,17 @@ describe("PosthogAnalytics", () => { }); }); - it("Should silently not send messages if not inititalised", () => { - analytics.track("jest_test_event", { + it("Should silently not track if not inititalised", () => { + analytics.trackAnonymousEvent("jest_test_event", { foo: "bar", }); expect(fakePosthog.capture.mock.calls.length).toBe(0); }); + + it("Should not track non-anonymous messages if onlyTrackAnonymousEvents is true", () => { + analytics.trackAnonymousEvent("jest_test_event", { + foo: "bar", + }); + }); }); From 4b0cb409a078e0baa701e91ff88eae2d49e41bb9 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 08:38:58 +0100 Subject: [PATCH 005/115] Add identifyUser --- src/PosthogAnalytics.ts | 5 +++++ test/PosthogAnalytics-test.ts | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 133c9275d4..404f0e5f20 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -64,6 +64,11 @@ export class PosthogAnalytics { } } + public async identifyUser(userId: string) { + if (this.onlyTrackAnonymousEvents) return; + this.posthog.identify(await hashHex(userId)); + } + public isInitialised(): boolean { return this.initialised; } diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index dfadac921d..fd49255fa1 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -5,10 +5,12 @@ const crypto = require('crypto'); class FakePosthog { public capture; public init; + public identify; constructor() { this.capture = jest.fn(); this.init = jest.fn(); + this.identify = jest.fn(); } } @@ -75,7 +77,7 @@ describe("PosthogAnalytics", () => { expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ foo: "bar" }); }); - it("Should pass trackRoomEvent to posthog", () => { + it("Should pass trackRoomEvent to posthog", async () => { analytics.init(false); const roomId = "42"; return analytics.trackRoomEvent("jest_test_event", roomId, { @@ -102,4 +104,18 @@ describe("PosthogAnalytics", () => { foo: "bar", }); }); + + it("Should identify the user to posthog if onlyTrackAnonymousEvents is false", async () => { + analytics.init(false); + await analytics.identifyUser("foo"); + expect(fakePosthog.identify.mock.calls[0][0]) + .toBe("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"); + }); + + it("Should not identify the user to posthog if onlyTrackAnonymousEvents is true", async () => { + analytics.init(true); + return analytics.identifyUser("foo").then(() => { + expect(fakePosthog.identify.mock.calls.length).toBe(0); + }); + }); }); From b5564a0de08088ad59179f7948c110b118f52e54 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 08:42:29 +0100 Subject: [PATCH 006/115] Add getAnalytics helper --- src/PosthogAnalytics.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 404f0e5f20..0195142d9b 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -106,3 +106,7 @@ export class PosthogAnalytics { this.trackPseudonymousEvent(eventName, updatedProperties); } } + +export default function getAnalytics() { + return PosthogAnalytics.instance(); +} From 678474c0e8abac4bb1353329ed85aebe835def03 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 11:23:18 +0100 Subject: [PATCH 007/115] Fix missing underscore --- src/PosthogAnalytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 0195142d9b..1b5988e0df 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -41,7 +41,7 @@ export class PosthogAnalytics { private static _instance = null; public static instance(): PosthogAnalytics { - if (!this.instance) { + if (!this._instance) { this._instance = new PosthogAnalytics(posthog); } return this._instance; From d9594c428a8defd13ca31dd4200619bbf39c6141 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 11:23:42 +0100 Subject: [PATCH 008/115] login event should be IAnonymousEvent --- src/PosthogAnalytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 1b5988e0df..d1149095e0 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -23,7 +23,7 @@ export interface IRoomEvent extends IPseudonymousEvent { hashedRoomId: string } -export interface IOnboardingLoginBegin extends IEvent { +export interface IOnboardingLoginBegin extends IAnonymousEvent { key: "onboarding_login_begin", } From 7e549f84e7da7a6286c1cfc1f806f9b0b1fb91fc Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 11:23:55 +0100 Subject: [PATCH 009/115] Don't make getAnalytics the default export, its weird --- src/PosthogAnalytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index d1149095e0..80b5861c17 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -107,6 +107,6 @@ export class PosthogAnalytics { } } -export default function getAnalytics() { +export function getAnalytics(): PosthogAnalytics { return PosthogAnalytics.instance(); } From 6da3cc8ca1baf268d768ed63e4b9cb16e40ce33d Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 13:48:10 +0100 Subject: [PATCH 010/115] Redact sensitive data --- src/PosthogAnalytics.ts | 103 +++++++++++++++++++++++++++++----- test/PosthogAnalytics-test.ts | 66 ++++++++++++++++------ 2 files changed, 138 insertions(+), 31 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 80b5861c17..d5f9b1d83c 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -1,4 +1,4 @@ -import posthog from 'posthog-js'; +import posthog, { PostHog } from 'posthog-js'; import SdkConfig from './SdkConfig'; interface IEvent { @@ -10,6 +10,11 @@ interface IEvent { properties: {} } +export enum Anonymity { + Anonymous, + Pseudonymous +} + // If an event extends IPseudonymousEvent, the event contains pseudonymous data // that won't be sent unless the user has explicitly consented to pseudonymous tracking. // For example, hashed user IDs or room IDs. @@ -33,10 +38,38 @@ const hashHex = async (input: string): Promise => { return [...new Uint8Array(digestBuf)].map((b: number) => b.toString(16).padStart(2, "0")).join(""); }; +const knownScreens = new Set([ + "register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory", + "start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group", +]); + +export async function getRedactedCurrentLocation(origin: string, hash: string, pathname: string, anonymity: Anonymity) { + // Redact PII from the current location. + // If anonymous is true, redact entirely, if false, substitute it with a hash. + // For known screens, assumes a URL structure of //might/be/pii + if (origin.startsWith('file://')) { + pathname = "//"; + } + + let [_, screen, ...parts] = hash.split("/"); + + if (!knownScreens.has(screen)) { + screen = ""; + } + + for (let i = 0; i < parts.length; i++) { + parts[i] = anonymity === Anonymity.Anonymous ? `` : await hashHex(parts[i]); + } + + const hashStr = `${_}/${screen}/${parts.join("/")}`; + return origin + pathname + hashStr; +} + export class PosthogAnalytics { private onlyTrackAnonymousEvents = false; private initialised = false; - private posthog = null; + private posthog?: PostHog = null; + private redactedCurrentLocation = null; private static _instance = null; @@ -47,23 +80,63 @@ export class PosthogAnalytics { return this._instance; } - constructor(posthog) { + constructor(posthog: PostHog) { this.posthog = posthog; } - public init(onlyTrackAnonymousEvents: boolean) { + public async init(onlyTrackAnonymousEvents: boolean) { if (Boolean(navigator.doNotTrack === "1")) { this.initialised = false; return; } this.onlyTrackAnonymousEvents = onlyTrackAnonymousEvents; + const posthogConfig = SdkConfig.get()["posthog"]; if (posthogConfig) { - this.posthog.init(posthogConfig.projectApiKey, { api_host: posthogConfig.apiHost }); + // Update the redacted current location before initialising posthog, as posthog.init triggers + // an immediate pageview event which calls the sanitize_properties callback + await this.updateRedactedCurrentLocation(); + + this.posthog.init(posthogConfig.projectApiKey, { + api_host: posthogConfig.apiHost, + autocapture: false, + mask_all_text: true, + mask_all_element_attributes: true, + sanitize_properties: this.sanitizeProperties.bind(this), + }); this.initialised = true; } } + private async updateRedactedCurrentLocation() { + // TODO only calculate this when the location changes as its expensive + const { origin, hash, pathname } = window.location; + this.redactedCurrentLocation = await getRedactedCurrentLocation( + origin, hash, pathname, this.onlyTrackAnonymousEvents ? Anonymity.Anonymous : Anonymity.Pseudonymous); + } + + private sanitizeProperties(properties: posthog.Properties, _: string): posthog.Properties { + // Sanitize posthog's built in properties which leak PII e.g. url reporting + // see utils.js _.info.properties in posthog-js + + // this.redactedCurrentLocation needs to have been updated prior to reaching this point as + // updating it involves async, which this callback is not + properties['$current_url'] = this.redactedCurrentLocation; + + if (this.onlyTrackAnonymousEvents) { + // drop referrer information for anonymous users + properties['$referrer'] = null; + properties['$referring_domain'] = null; + properties['$initial_referrer'] = null; + properties['$initial_referring_domain'] = null; + + // drop device ID, which is a UUID persisted in local storage + properties['$device_id'] = null; + } + + return properties; + } + public async identifyUser(userId: string) { if (this.onlyTrackAnonymousEvents) return; this.posthog.identify(await hashHex(userId)); @@ -77,21 +150,25 @@ export class PosthogAnalytics { this.onlyTrackAnonymousEvents = enabled; } - public trackPseudonymousEvent( - eventName: E["eventName"], - properties: E["properties"], - ) { + private async capture(eventName: string, properties: posthog.Properties, anonymity: Anonymity) { if (!this.initialised) return; - if (this.onlyTrackAnonymousEvents) return; + await this.updateRedactedCurrentLocation(anonymity); this.posthog.capture(eventName, properties); } - public trackAnonymousEvent( + public async trackPseudonymousEvent( eventName: E["eventName"], properties: E["properties"], ) { - if (!this.initialised) return; - this.posthog.capture(eventName, properties); + if (this.onlyTrackAnonymousEvents) return; + this.capture(eventName, properties, Anonymity.Pseudonyomous); + } + + public async trackAnonymousEvent( + eventName: E["eventName"], + properties: E["properties"], + ) { + this.capture(eventName, properties, Anonymity.Anonymous); } public async trackRoomEvent( diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index fd49255fa1..e9efeffa7d 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -1,4 +1,5 @@ -import { IAnonymousEvent, IRoomEvent, PosthogAnalytics } from '../src/PosthogAnalytics'; +import { Anonymity, getRedactedCurrentLocation, IAnonymousEvent, IRoomEvent, + PosthogAnalytics } from '../src/PosthogAnalytics'; import SdkConfig from '../src/SdkConfig'; const crypto = require('crypto'); @@ -68,9 +69,9 @@ describe("PosthogAnalytics", () => { expect(analytics.isInitialised()).toBe(true); }); - it("Should pass track() to posthog", () => { + it("Should pass track() to posthog", async () => { analytics.init(false); - analytics.trackAnonymousEvent("jest_test_event", { + await analytics.trackAnonymousEvent("jest_test_event", { foo: "bar", }); expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); @@ -80,29 +81,29 @@ describe("PosthogAnalytics", () => { it("Should pass trackRoomEvent to posthog", async () => { analytics.init(false); const roomId = "42"; - return analytics.trackRoomEvent("jest_test_event", roomId, { + await analytics.trackRoomEvent("jest_test_event", roomId, { foo: "bar", - }).then(() => { - expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); - expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ - foo: "bar", - hashedRoomId: "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049", - }); + }); + expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); + expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ + foo: "bar", + hashedRoomId: "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049", }); }); - it("Should silently not track if not inititalised", () => { - analytics.trackAnonymousEvent("jest_test_event", { + it("Should silently not track if not inititalised", async () => { + await analytics.trackAnonymousEvent("jest_test_event", { foo: "bar", }); - expect(fakePosthog.capture.mock.calls.length).toBe(0); }); - it("Should not track non-anonymous messages if onlyTrackAnonymousEvents is true", () => { - analytics.trackAnonymousEvent("jest_test_event", { + it("Should not track non-anonymous messages if onlyTrackAnonymousEvents is true", async () => { + analytics.init(true); + await analytics.trackPseudonymousEvent("jest_test_event", { foo: "bar", }); + expect(fakePosthog.capture.mock.calls.length).toBe(0); }); it("Should identify the user to posthog if onlyTrackAnonymousEvents is false", async () => { @@ -114,8 +115,37 @@ describe("PosthogAnalytics", () => { it("Should not identify the user to posthog if onlyTrackAnonymousEvents is true", async () => { analytics.init(true); - return analytics.identifyUser("foo").then(() => { - expect(fakePosthog.identify.mock.calls.length).toBe(0); - }); + await analytics.identifyUser("foo"); + expect(fakePosthog.identify.mock.calls.length).toBe(0); + }); + + it("Should pseudonymise a location of a known screen", async () => { + const location = await getRedactedCurrentLocation( + "https://foo.bar", "#/register/some/pii", "/", Anonymity.Pseudonymous); + expect(location).toBe( + `https://foo.bar/#/register/\ +a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b/\ +bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`); + }); + + it("Should anonymise a location of a known screen", async () => { + const location = await getRedactedCurrentLocation( + "https://foo.bar", "#/register/some/pii", "/", Anonymity.Anonymous); + expect(location).toBe("https://foo.bar/#/register//"); + }); + + it("Should pseudonymise a location of an unknown screen", async () => { + const location = await getRedactedCurrentLocation( + "https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Pseudonymous); + expect(location).toBe( + `https://foo.bar/#//\ +a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b/\ +bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`); + }); + + it("Should anonymise a location of an unknown screen", async () => { + const location = await getRedactedCurrentLocation( + "https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Anonymous); + expect(location).toBe("https://foo.bar/#///"); }); }); From 4c6b0d35add7ae8d58f71ea1711587e31081444b Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 14:57:24 +0100 Subject: [PATCH 011/115] Improve analytics interface * Make it an error to call it before its initialised, and separately track whether its been enabled * Use anonmity enum in the public interface * Properly await upstream calls * Fix accidental test fixture cross-reliance --- src/PosthogAnalytics.ts | 60 ++++++++---- test/PosthogAnalytics-test.ts | 177 ++++++++++++++++++---------------- 2 files changed, 136 insertions(+), 101 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index d5f9b1d83c..63cb3bc422 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -66,10 +66,11 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p } export class PosthogAnalytics { - private onlyTrackAnonymousEvents = false; + private anonymity = Anonymity.Anonymous; private initialised = false; private posthog?: PostHog = null; private redactedCurrentLocation = null; + private enabled = false; private static _instance = null; @@ -84,12 +85,8 @@ export class PosthogAnalytics { this.posthog = posthog; } - public async init(onlyTrackAnonymousEvents: boolean) { - if (Boolean(navigator.doNotTrack === "1")) { - this.initialised = false; - return; - } - this.onlyTrackAnonymousEvents = onlyTrackAnonymousEvents; + public async init(anonymity: Anonymity) { + this.anonymity = Boolean(navigator.doNotTrack === "1") ? Anonymity.Anonymous : anonymity; const posthogConfig = SdkConfig.get()["posthog"]; if (posthogConfig) { @@ -105,6 +102,9 @@ export class PosthogAnalytics { sanitize_properties: this.sanitizeProperties.bind(this), }); this.initialised = true; + this.enabled = true; + } else { + this.enabled = false; } } @@ -112,7 +112,7 @@ export class PosthogAnalytics { // TODO only calculate this when the location changes as its expensive const { origin, hash, pathname } = window.location; this.redactedCurrentLocation = await getRedactedCurrentLocation( - origin, hash, pathname, this.onlyTrackAnonymousEvents ? Anonymity.Anonymous : Anonymity.Pseudonymous); + origin, hash, pathname, this.anonymity); } private sanitizeProperties(properties: posthog.Properties, _: string): posthog.Properties { @@ -123,7 +123,7 @@ export class PosthogAnalytics { // updating it involves async, which this callback is not properties['$current_url'] = this.redactedCurrentLocation; - if (this.onlyTrackAnonymousEvents) { + if (this.anonymity == Anonymity.Anonymous) { // drop referrer information for anonymous users properties['$referrer'] = null; properties['$referring_domain'] = null; @@ -138,21 +138,41 @@ export class PosthogAnalytics { } public async identifyUser(userId: string) { - if (this.onlyTrackAnonymousEvents) return; + if (this.anonymity == Anonymity.Anonymous) return; this.posthog.identify(await hashHex(userId)); } - public isInitialised(): boolean { + public isInitialised() { return this.initialised; } - public setOnlyTrackAnonymousEvents(enabled: boolean) { - this.onlyTrackAnonymousEvents = enabled; + public isEnabled() { + return this.enabled; } - private async capture(eventName: string, properties: posthog.Properties, anonymity: Anonymity) { - if (!this.initialised) return; - await this.updateRedactedCurrentLocation(anonymity); + public setAnonymity(anonymity: Anonymity) { + this.anonymity = anonymity; + } + + public getAnonymity() { + return this.anonymity; + } + + public logout() { + if (this.enabled) { + this.posthog.reset(); + } + this.setAnonymity(Anonymity.Anonymous); + } + + private async capture(eventName: string, properties: posthog.Properties) { + if (!this.enabled) { + return; + } + if (!this.initialised) { + throw Error("Tried to track event before PoshogAnalytics.init has completed"); + } + await this.updateRedactedCurrentLocation(); this.posthog.capture(eventName, properties); } @@ -160,15 +180,15 @@ export class PosthogAnalytics { eventName: E["eventName"], properties: E["properties"], ) { - if (this.onlyTrackAnonymousEvents) return; - this.capture(eventName, properties, Anonymity.Pseudonyomous); + if (this.anonymity == Anonymity.Anonymous) return; + await this.capture(eventName, properties); } public async trackAnonymousEvent( eventName: E["eventName"], properties: E["properties"], ) { - this.capture(eventName, properties, Anonymity.Anonymous); + await this.capture(eventName, properties); } public async trackRoomEvent( @@ -180,7 +200,7 @@ export class PosthogAnalytics { ...properties, hashedRoomId: roomId ? await hashHex(roomId) : null, }; - this.trackPseudonymousEvent(eventName, updatedProperties); + await this.trackPseudonymousEvent(eventName, updatedProperties); } } diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index e9efeffa7d..515d51b8e4 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -46,106 +46,121 @@ describe("PosthogAnalytics", () => { window.crypto = null; }); - it("Should not initialise if DNT is enabled", () => { - navigator.doNotTrack = "1"; - analytics.init(false); - expect(analytics.isInitialised()).toBe(false); - }); - - it("Should not initialise if config is not set", () => { - jest.spyOn(SdkConfig, "get").mockReturnValue({}); - analytics.init(false); - expect(analytics.isInitialised()).toBe(false); - }); - - it("Should initialise if config is set", () => { - jest.spyOn(SdkConfig, "get").mockReturnValue({ - posthog: { - projectApiKey: "foo", - apiHost: "bar", - }, + describe("Initialisation", () => { + it("Should not initialise if config is not set", async () => { + jest.spyOn(SdkConfig, "get").mockReturnValue({}); + await analytics.init(Anonymity.Pseudonymous); + expect(analytics.isEnabled()).toBe(false); }); - analytics.init(false); - expect(analytics.isInitialised()).toBe(true); - }); - it("Should pass track() to posthog", async () => { - analytics.init(false); - await analytics.trackAnonymousEvent("jest_test_event", { - foo: "bar", + it("Should initialise if config is set", async () => { + jest.spyOn(SdkConfig, "get").mockReturnValue({ + posthog: { + projectApiKey: "foo", + apiHost: "bar", + }, + }); + await analytics.init(Anonymity.Pseudonymous); + expect(analytics.isInitialised()).toBe(true); + expect(analytics.isEnabled()).toBe(true); }); - expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); - expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ foo: "bar" }); - }); - it("Should pass trackRoomEvent to posthog", async () => { - analytics.init(false); - const roomId = "42"; - await analytics.trackRoomEvent("jest_test_event", roomId, { - foo: "bar", - }); - expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); - expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ - foo: "bar", - hashedRoomId: "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049", + it("Should force anonymous if DNT is enabled", async () => { + navigator.doNotTrack = "1"; + await analytics.init(Anonymity.Pseudonymous); + expect(analytics.getAnonymity()).toBe(Anonymity.Anonymous); }); }); - it("Should silently not track if not inititalised", async () => { - await analytics.trackAnonymousEvent("jest_test_event", { - foo: "bar", + describe("Tracking", () => { + beforeEach(() => { + navigator.doNotTrack = null; + jest.spyOn(SdkConfig, "get").mockReturnValue({ + posthog: { + projectApiKey: "foo", + apiHost: "bar", + }, + }); }); - expect(fakePosthog.capture.mock.calls.length).toBe(0); - }); - it("Should not track non-anonymous messages if onlyTrackAnonymousEvents is true", async () => { - analytics.init(true); - await analytics.trackPseudonymousEvent("jest_test_event", { - foo: "bar", + it("Should pass track() to posthog", async () => { + await analytics.init(Anonymity.Pseudonymous); + await analytics.trackAnonymousEvent("jest_test_event", { + foo: "bar", + }); + expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); + expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ foo: "bar" }); }); - expect(fakePosthog.capture.mock.calls.length).toBe(0); - }); - it("Should identify the user to posthog if onlyTrackAnonymousEvents is false", async () => { - analytics.init(false); - await analytics.identifyUser("foo"); - expect(fakePosthog.identify.mock.calls[0][0]) - .toBe("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"); - }); + it("Should pass trackRoomEvent to posthog", async () => { + await analytics.init(Anonymity.Pseudonymous); + const roomId = "42"; + await analytics.trackRoomEvent("jest_test_event", roomId, { + foo: "bar", + }); + expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); + expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ + foo: "bar", + hashedRoomId: "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049", + }); + }); - it("Should not identify the user to posthog if onlyTrackAnonymousEvents is true", async () => { - analytics.init(true); - await analytics.identifyUser("foo"); - expect(fakePosthog.identify.mock.calls.length).toBe(0); - }); + it("Should silently not track if not inititalised", async () => { + await analytics.trackAnonymousEvent("jest_test_event", { + foo: "bar", + }); + expect(fakePosthog.capture.mock.calls.length).toBe(0); + }); - it("Should pseudonymise a location of a known screen", async () => { - const location = await getRedactedCurrentLocation( - "https://foo.bar", "#/register/some/pii", "/", Anonymity.Pseudonymous); - expect(location).toBe( - `https://foo.bar/#/register/\ + it("Should not track non-anonymous messages if anonymous", async () => { + await analytics.init(Anonymity.Anonymous); + await analytics.trackPseudonymousEvent("jest_test_event", { + foo: "bar", + }); + expect(fakePosthog.capture.mock.calls.length).toBe(0); + }); + + it("Should pseudonymise a location of a known screen", async () => { + const location = await getRedactedCurrentLocation( + "https://foo.bar", "#/register/some/pii", "/", Anonymity.Pseudonymous); + expect(location).toBe( + `https://foo.bar/#/register/\ a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b/\ bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`); - }); + }); - it("Should anonymise a location of a known screen", async () => { - const location = await getRedactedCurrentLocation( - "https://foo.bar", "#/register/some/pii", "/", Anonymity.Anonymous); - expect(location).toBe("https://foo.bar/#/register//"); - }); + it("Should anonymise a location of a known screen", async () => { + const location = await getRedactedCurrentLocation( + "https://foo.bar", "#/register/some/pii", "/", Anonymity.Anonymous); + expect(location).toBe("https://foo.bar/#/register//"); + }); - it("Should pseudonymise a location of an unknown screen", async () => { - const location = await getRedactedCurrentLocation( - "https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Pseudonymous); - expect(location).toBe( - `https://foo.bar/#//\ + it("Should pseudonymise a location of an unknown screen", async () => { + const location = await getRedactedCurrentLocation( + "https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Pseudonymous); + expect(location).toBe( + `https://foo.bar/#//\ a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b/\ bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`); - }); + }); - it("Should anonymise a location of an unknown screen", async () => { - const location = await getRedactedCurrentLocation( - "https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Anonymous); - expect(location).toBe("https://foo.bar/#///"); + it("Should anonymise a location of an unknown screen", async () => { + const location = await getRedactedCurrentLocation( + "https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Anonymous); + expect(location).toBe("https://foo.bar/#///"); + }); + + it("Should identify the user to posthog if pseudonymous", async () => { + await analytics.init(Anonymity.Pseudonymous); + await analytics.identifyUser("foo"); + expect(fakePosthog.identify.mock.calls[0][0]) + .toBe("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"); + }); + + it("Should not identify the user to posthog if anonymous", async () => { + await analytics.init(Anonymity.Anonymous); + await analytics.identifyUser("foo"); + expect(fakePosthog.identify.mock.calls.length).toBe(0); + }); }); }); From 726b4497b2a54fcf6fe743d988325879d8e77890 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 16:06:09 +0100 Subject: [PATCH 012/115] Remove redactedCurrentLocation and rely on posthog for DNT * Redact and pass the redacted url as a property. redactedCurrentLocation might have issues with concurrent events * Remove DNT code and rely on posthog --- src/PosthogAnalytics.ts | 42 +++++++++++++++++------------------ test/PosthogAnalytics-test.ts | 30 +++++++++---------------- 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 63cb3bc422..fd8bb44e0b 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -69,7 +69,6 @@ export class PosthogAnalytics { private anonymity = Anonymity.Anonymous; private initialised = false; private posthog?: PostHog = null; - private redactedCurrentLocation = null; private enabled = false; private static _instance = null; @@ -85,21 +84,20 @@ export class PosthogAnalytics { this.posthog = posthog; } - public async init(anonymity: Anonymity) { - this.anonymity = Boolean(navigator.doNotTrack === "1") ? Anonymity.Anonymous : anonymity; - + public init(anonymity: Anonymity) { const posthogConfig = SdkConfig.get()["posthog"]; if (posthogConfig) { - // Update the redacted current location before initialising posthog, as posthog.init triggers - // an immediate pageview event which calls the sanitize_properties callback - await this.updateRedactedCurrentLocation(); - this.posthog.init(posthogConfig.projectApiKey, { api_host: posthogConfig.apiHost, autocapture: false, mask_all_text: true, mask_all_element_attributes: true, + // this is disabled for now as its tricky to sanitize properties of the pageview + // event because sanitization requires async crypto calls and the sanitize_properties + // callback is synchronous. + capture_pageview: false, sanitize_properties: this.sanitizeProperties.bind(this), + respect_dnt: true, }); this.initialised = true; this.enabled = true; @@ -108,20 +106,20 @@ export class PosthogAnalytics { } } - private async updateRedactedCurrentLocation() { - // TODO only calculate this when the location changes as its expensive - const { origin, hash, pathname } = window.location; - this.redactedCurrentLocation = await getRedactedCurrentLocation( - origin, hash, pathname, this.anonymity); - } - private sanitizeProperties(properties: posthog.Properties, _: string): posthog.Properties { - // Sanitize posthog's built in properties which leak PII e.g. url reporting - // see utils.js _.info.properties in posthog-js + // Callback from posthog to sanitize properties before sending them to the server. + // + // Here we sanitize posthog's built in properties which leak PII e.g. url reporting. + // See utils.js _.info.properties in posthog-js. - // this.redactedCurrentLocation needs to have been updated prior to reaching this point as - // updating it involves async, which this callback is not - properties['$current_url'] = this.redactedCurrentLocation; + // Replace the $current_url with a redacted version. + // $redacted_current_url is injected by this class earlier in capture(), as its generation + // is async and can't be done in this non-async callback. + if (!properties['$redacted_current_url']) { + console.log("$redacted_current_url not set in sanitizeProperties, will drop $current_url entirely"); + } + properties['$current_url'] = properties['$redacted_current_url']; + delete properties['$redacted_current_url']; if (this.anonymity == Anonymity.Anonymous) { // drop referrer information for anonymous users @@ -172,7 +170,9 @@ export class PosthogAnalytics { if (!this.initialised) { throw Error("Tried to track event before PoshogAnalytics.init has completed"); } - await this.updateRedactedCurrentLocation(); + const { origin, hash, pathname } = window.location; + properties['$redacted_current_url'] = await getRedactedCurrentLocation( + origin, hash, pathname, this.anonymity); this.posthog.capture(eventName, properties); } diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index 515d51b8e4..a1e54dc05d 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -49,7 +49,7 @@ describe("PosthogAnalytics", () => { describe("Initialisation", () => { it("Should not initialise if config is not set", async () => { jest.spyOn(SdkConfig, "get").mockReturnValue({}); - await analytics.init(Anonymity.Pseudonymous); + analytics.init(Anonymity.Pseudonymous); expect(analytics.isEnabled()).toBe(false); }); @@ -60,21 +60,14 @@ describe("PosthogAnalytics", () => { apiHost: "bar", }, }); - await analytics.init(Anonymity.Pseudonymous); + analytics.init(Anonymity.Pseudonymous); expect(analytics.isInitialised()).toBe(true); expect(analytics.isEnabled()).toBe(true); }); - - it("Should force anonymous if DNT is enabled", async () => { - navigator.doNotTrack = "1"; - await analytics.init(Anonymity.Pseudonymous); - expect(analytics.getAnonymity()).toBe(Anonymity.Anonymous); - }); }); describe("Tracking", () => { beforeEach(() => { - navigator.doNotTrack = null; jest.spyOn(SdkConfig, "get").mockReturnValue({ posthog: { projectApiKey: "foo", @@ -84,25 +77,24 @@ describe("PosthogAnalytics", () => { }); it("Should pass track() to posthog", async () => { - await analytics.init(Anonymity.Pseudonymous); + analytics.init(Anonymity.Pseudonymous); await analytics.trackAnonymousEvent("jest_test_event", { foo: "bar", }); expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); - expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ foo: "bar" }); + expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar"); }); it("Should pass trackRoomEvent to posthog", async () => { - await analytics.init(Anonymity.Pseudonymous); + analytics.init(Anonymity.Pseudonymous); const roomId = "42"; await analytics.trackRoomEvent("jest_test_event", roomId, { foo: "bar", }); expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event"); - expect(fakePosthog.capture.mock.calls[0][1]).toEqual({ - foo: "bar", - hashedRoomId: "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049", - }); + expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar"); + expect(fakePosthog.capture.mock.calls[0][1]["hashedRoomId"]) + .toEqual("73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049"); }); it("Should silently not track if not inititalised", async () => { @@ -113,7 +105,7 @@ describe("PosthogAnalytics", () => { }); it("Should not track non-anonymous messages if anonymous", async () => { - await analytics.init(Anonymity.Anonymous); + analytics.init(Anonymity.Anonymous); await analytics.trackPseudonymousEvent("jest_test_event", { foo: "bar", }); @@ -151,14 +143,14 @@ bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`); }); it("Should identify the user to posthog if pseudonymous", async () => { - await analytics.init(Anonymity.Pseudonymous); + analytics.init(Anonymity.Pseudonymous); await analytics.identifyUser("foo"); expect(fakePosthog.identify.mock.calls[0][0]) .toBe("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"); }); it("Should not identify the user to posthog if anonymous", async () => { - await analytics.init(Anonymity.Anonymous); + analytics.init(Anonymity.Anonymous); await analytics.identifyUser("foo"); expect(fakePosthog.identify.mock.calls.length).toBe(0); }); From 5697eeaab8aab98d5a8294775ef60a4bc0ebf577 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 16:08:00 +0100 Subject: [PATCH 013/115] Put back accidentally removed anonymity update --- src/PosthogAnalytics.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index fd8bb44e0b..2e5b9446de 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -85,6 +85,7 @@ export class PosthogAnalytics { } public init(anonymity: Anonymity) { + this.anonymity = anonymity; const posthogConfig = SdkConfig.get()["posthog"]; if (posthogConfig) { this.posthog.init(posthogConfig.projectApiKey, { From 53b6749f73d5b8f545024f64597d267dc7aa18ec Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 16:09:22 +0100 Subject: [PATCH 014/115] Change onboarding_login_begin to welcome_screen_load --- src/PosthogAnalytics.ts | 4 ++-- src/components/views/auth/Welcome.js | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 2e5b9446de..faf9af6ba5 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -28,8 +28,8 @@ export interface IRoomEvent extends IPseudonymousEvent { hashedRoomId: string } -export interface IOnboardingLoginBegin extends IAnonymousEvent { - key: "onboarding_login_begin", +export interface IWelcomeScreenLoad extends IAnonymousEvent { + key: "welcome_screen_load", } const hashHex = async (input: string): Promise => { diff --git a/src/components/views/auth/Welcome.js b/src/components/views/auth/Welcome.js index e3f7a601f2..470ea4223c 100644 --- a/src/components/views/auth/Welcome.js +++ b/src/components/views/auth/Welcome.js @@ -25,6 +25,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; import CountlyAnalytics from "../../../CountlyAnalytics"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { getAnalytics } from "../../../PosthogAnalytics"; // translatable strings for Welcome pages _td("Sign in with SSO"); @@ -68,4 +69,8 @@ export default class Welcome extends React.PureComponent { ); } + + componentDidMount() { + getAnalytics().trackAnonymousEvent("welcome_screen_load", {}); + } } From 6737cfd2978e44c47d8eef73375457c0b91f4505 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 16:10:06 +0100 Subject: [PATCH 015/115] remove superflous dnt clear --- test/PosthogAnalytics-test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index a1e54dc05d..a33544e738 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -42,7 +42,6 @@ describe("PosthogAnalytics", () => { }); afterEach(() => { - navigator.doNotTrack = null; window.crypto = null; }); From 34f8c60b346a76ac3511999c7f6f256f4231d17e Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 16:15:18 +0100 Subject: [PATCH 016/115] Hook analytics into the SDK --- src/Lifecycle.ts | 3 +++ src/components/structures/MatrixChat.tsx | 5 +++++ .../views/settings/tabs/user/SecurityUserSettingsTab.js | 2 ++ 3 files changed, 10 insertions(+) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 410124a637..8536f808ff 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -48,6 +48,7 @@ import { Jitsi } from "./widgets/Jitsi"; import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY } from "./BasePlatform"; import ThreepidInviteStore from "./stores/ThreepidInviteStore"; import CountlyAnalytics from "./CountlyAnalytics"; +import { getAnalytics } from "./PosthogAnalytics"; import CallHandler from './CallHandler'; import LifecycleCustomisations from "./customisations/Lifecycle"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; @@ -700,6 +701,8 @@ export function logout(): void { CountlyAnalytics.instance.enable(/* anonymous = */ true); } + getAnalytics().logout(); + if (MatrixClientPeg.get().isGuest()) { // logout doesn't work for guest sessions // Also we sometimes want to re-log in a guest session if we abort the login. diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 785838ffca..3edc463a23 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -107,6 +107,7 @@ import UIStore, { UI_EVENTS } from "../../stores/UIStore"; import SoftLogout from './auth/SoftLogout'; import { makeRoomPermalink } from "../../utils/permalinks/Permalinks"; import { copyPlaintext } from "../../utils/strings"; +import { Anonymity, getAnalytics } from '../../PosthogAnalytics'; /** constants for MatrixChat.state.view */ export enum Views { @@ -387,6 +388,7 @@ export default class MatrixChat extends React.PureComponent { if (SettingsStore.getValue("analyticsOptIn")) { Analytics.enable(); } + getAnalytics().init(SettingsStore.getValue("analyticsOptIn") ? Anonymity.Pseudonymous : Anonymity.Anonymous); CountlyAnalytics.instance.enable(/* anonymous = */ true); } @@ -498,6 +500,8 @@ export default class MatrixChat extends React.PureComponent { } else if (SettingsStore.getValue("analyticsOptIn")) { CountlyAnalytics.instance.enable(/* anonymous = */ false); } + getAnalytics().setAnonymity(SettingsStore.getValue("analyticsOptIn") ? + Anonymity.Pseudonymous: Anonymity.Anonymous); }); // Note we don't catch errors from this: we catch everything within // loadSession as there's logic there to ask the user if they want @@ -822,6 +826,7 @@ export default class MatrixChat extends React.PureComponent { if (CountlyAnalytics.instance.canEnable()) { CountlyAnalytics.instance.enable(/* anonymous = */ false); } + getAnalytics().setAnonymity(Anonymity.Pseudonymous); break; case 'reject_cookies': SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false); diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index a03598b21f..15b4992cd8 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -36,6 +36,7 @@ import { UIFeature } from "../../../../../settings/UIFeature"; import { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel"; import CountlyAnalytics from "../../../../../CountlyAnalytics"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +import { Anonymity, getAnalytics } from "../../../../../PosthogAnalytics"; export class IgnoredUser extends React.Component { static propTypes = { @@ -106,6 +107,7 @@ export default class SecurityUserSettingsTab extends React.Component { _updateAnalytics = (checked) => { checked ? Analytics.enable() : Analytics.disable(); CountlyAnalytics.instance.enable(/* anonymous = */ !checked); + getAnalytics().setAnonymity(checked ? Anonymity.Pseudonymous : Anonymity.Anonymous); }; _onExportE2eKeysClicked = () => { From 93962c0acaa0887aef2a5bf425226d8004798fcd Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 16:29:11 +0100 Subject: [PATCH 017/115] Update reasoning around disabling capture_pageview --- src/PosthogAnalytics.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index faf9af6ba5..c18cb98f03 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -93,9 +93,11 @@ export class PosthogAnalytics { autocapture: false, mask_all_text: true, mask_all_element_attributes: true, - // this is disabled for now as its tricky to sanitize properties of the pageview - // event because sanitization requires async crypto calls and the sanitize_properties - // callback is synchronous. + // This only triggers on page load, which for our SPA isn't particularly useful. + // Plus, the .capture call originating from somewhere in posthog makes it hard + // to redact URLs, which requires async code. + // + // To raise this manually, just call .capture("$pageview") or posthog.capture_pageview. capture_pageview: false, sanitize_properties: this.sanitizeProperties.bind(this), respect_dnt: true, From f358deb6c4dc2281993d275f5c8fdcff7d328da1 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 16:52:57 +0100 Subject: [PATCH 018/115] Manually track page views --- src/PosthogAnalytics.ts | 13 +++++++++++++ src/components/structures/MatrixChat.tsx | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index c18cb98f03..026c8d9c5e 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -28,6 +28,13 @@ export interface IRoomEvent extends IPseudonymousEvent { hashedRoomId: string } +interface IPageView extends IAnonymousEvent { + eventName: "$pageview", + properties: { + durationMs?: number + } +} + export interface IWelcomeScreenLoad extends IAnonymousEvent { key: "welcome_screen_load", } @@ -205,6 +212,12 @@ export class PosthogAnalytics { }; await this.trackPseudonymousEvent(eventName, updatedProperties); } + + public async trackPageView(durationMs: number) { + await this.trackAnonymousEvent("$pageview", { + durationMs, + }); + } } export function getAnalytics(): PosthogAnalytics { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 3edc463a23..b29ede409b 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -107,7 +107,7 @@ import UIStore, { UI_EVENTS } from "../../stores/UIStore"; import SoftLogout from './auth/SoftLogout'; import { makeRoomPermalink } from "../../utils/permalinks/Permalinks"; import { copyPlaintext } from "../../utils/strings"; -import { Anonymity, getAnalytics } from '../../PosthogAnalytics'; +import { Anonymity, getAnalytics, IPageChange } from '../../PosthogAnalytics'; /** constants for MatrixChat.state.view */ export enum Views { @@ -445,6 +445,7 @@ export default class MatrixChat extends React.PureComponent { const durationMs = this.stopPageChangeTimer(); Analytics.trackPageChange(durationMs); CountlyAnalytics.instance.trackPageChange(durationMs); + getAnalytics().trackPageView(durationMs); } if (this.focusComposer) { dis.fire(Action.FocusSendMessageComposer); From b380a89ac6ab72526faba13dabfd00ab4ca3cffb Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 17:01:45 +0100 Subject: [PATCH 019/115] Fix wrong overriden attribute --- src/PosthogAnalytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 026c8d9c5e..d49fb567a0 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -36,7 +36,7 @@ interface IPageView extends IAnonymousEvent { } export interface IWelcomeScreenLoad extends IAnonymousEvent { - key: "welcome_screen_load", + eventName: "welcome_screen_load", } const hashHex = async (input: string): Promise => { From e9e0e4847f45abda0eef86e98c9e01d75ceabe81 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 17:02:14 +0100 Subject: [PATCH 020/115] Move Welcome.js to tsx --- src/components/views/auth/{Welcome.js => Welcome.tsx} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/components/views/auth/{Welcome.js => Welcome.tsx} (93%) diff --git a/src/components/views/auth/Welcome.js b/src/components/views/auth/Welcome.tsx similarity index 93% rename from src/components/views/auth/Welcome.js rename to src/components/views/auth/Welcome.tsx index 470ea4223c..92fe9df4d5 100644 --- a/src/components/views/auth/Welcome.js +++ b/src/components/views/auth/Welcome.tsx @@ -25,7 +25,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; import CountlyAnalytics from "../../../CountlyAnalytics"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { getAnalytics } from "../../../PosthogAnalytics"; +import { getAnalytics, IWelcomeScreenLoad } from "../../../PosthogAnalytics"; // translatable strings for Welcome pages _td("Sign in with SSO"); @@ -71,6 +71,6 @@ export default class Welcome extends React.PureComponent { } componentDidMount() { - getAnalytics().trackAnonymousEvent("welcome_screen_load", {}); + getAnalytics().trackAnonymousEvent("welcome_screen_load", { foo: "bar" }); } } From 0c89eb51d4df8ce683584ec93d04d17bd9b11974 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 18:24:14 +0100 Subject: [PATCH 021/115] add registerSuperProperties --- src/PosthogAnalytics.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index d49fb567a0..3f13762327 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -150,6 +150,10 @@ export class PosthogAnalytics { this.posthog.identify(await hashHex(userId)); } + public registerSuperProperties(properties) { + this.posthog.register(properties); + } + public isInitialised() { return this.initialised; } From 585b702652d1ea1e86de130177867151bc29c5aa Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 18:25:05 +0100 Subject: [PATCH 022/115] Add tip about shasum --- src/PosthogAnalytics.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 3f13762327..8ebeff3565 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -40,6 +40,8 @@ export interface IWelcomeScreenLoad extends IAnonymousEvent { } const hashHex = async (input: string): Promise => { + // on os x (e.g. if you want to know the sha-256 of your own matrix ID so you can look it up): + // echo -n | shasum -a 256 const buf = new TextEncoder().encode(input); const digestBuf = await window.crypto.subtle.digest("sha-256", buf); return [...new Uint8Array(digestBuf)].map((b: number) => b.toString(16).padStart(2, "0")).join(""); From c34afdb4bdd834ea833016d0da795532c41b21b7 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 21 Jul 2021 18:35:25 +0100 Subject: [PATCH 023/115] Refactor platform properties loading --- src/Lifecycle.ts | 10 +++++++++- src/PosthogAnalytics.ts | 17 +++++++++++++++++ src/components/structures/MatrixChat.tsx | 14 ++++++++++---- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 8536f808ff..c27c774cd7 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -48,7 +48,7 @@ import { Jitsi } from "./widgets/Jitsi"; import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY } from "./BasePlatform"; import ThreepidInviteStore from "./stores/ThreepidInviteStore"; import CountlyAnalytics from "./CountlyAnalytics"; -import { getAnalytics } from "./PosthogAnalytics"; +import { Anonymity, getAnalytics, getPlatformProperties } from "./PosthogAnalytics"; import CallHandler from './CallHandler'; import LifecycleCustomisations from "./customisations/Lifecycle"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; @@ -574,6 +574,14 @@ async function doSetLoggedIn( await abortLogin(); } + if (SettingsStore.getValue("analyticsOptIn")) { + const analytics = getAnalytics(); + analytics.setAnonymity(Anonymity.Pseudonymous); + await analytics.identifyUser(credentials.userId); + } else { + getAnalytics().setAnonymity(Anonymity.Anonymous); + } + Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl); MatrixClientPeg.replaceUsingCreds(credentials); diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 8ebeff3565..9c167f5464 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -1,4 +1,5 @@ import posthog, { PostHog } from 'posthog-js'; +import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; interface IEvent { @@ -226,6 +227,22 @@ export class PosthogAnalytics { } } +export async function getPlatformProperties() { + const platform = PlatformPeg.get(); + let appVersion; + try { + appVersion = await platform.getAppVersion(); + } catch (e) { + // this happens if no version is set i.e. in dev + appVersion = "unknown"; + } + + return { + appVersion, + appPlatform: platform.getHumanReadableName(), + }; +} + export function getAnalytics(): PosthogAnalytics { return PosthogAnalytics.instance(); } diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index b29ede409b..513200520f 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -107,7 +107,7 @@ import UIStore, { UI_EVENTS } from "../../stores/UIStore"; import SoftLogout from './auth/SoftLogout'; import { makeRoomPermalink } from "../../utils/permalinks/Permalinks"; import { copyPlaintext } from "../../utils/strings"; -import { Anonymity, getAnalytics, IPageChange } from '../../PosthogAnalytics'; +import { Anonymity, getAnalytics, getPlatformProperties } from '../../PosthogAnalytics'; /** constants for MatrixChat.state.view */ export enum Views { @@ -388,7 +388,11 @@ export default class MatrixChat extends React.PureComponent { if (SettingsStore.getValue("analyticsOptIn")) { Analytics.enable(); } - getAnalytics().init(SettingsStore.getValue("analyticsOptIn") ? Anonymity.Pseudonymous : Anonymity.Anonymous); + + const analytics = getAnalytics(); + analytics.init(SettingsStore.getValue("analyticsOptIn") ? Anonymity.Pseudonymous : Anonymity.Anonymous); + getPlatformProperties().then((properties) => analytics.registerSuperProperties(properties)); + CountlyAnalytics.instance.enable(/* anonymous = */ true); } @@ -501,8 +505,6 @@ export default class MatrixChat extends React.PureComponent { } else if (SettingsStore.getValue("analyticsOptIn")) { CountlyAnalytics.instance.enable(/* anonymous = */ false); } - getAnalytics().setAnonymity(SettingsStore.getValue("analyticsOptIn") ? - Anonymity.Pseudonymous: Anonymity.Anonymous); }); // Note we don't catch errors from this: we catch everything within // loadSession as there's logic there to ask the user if they want @@ -828,6 +830,10 @@ export default class MatrixChat extends React.PureComponent { CountlyAnalytics.instance.enable(/* anonymous = */ false); } getAnalytics().setAnonymity(Anonymity.Pseudonymous); + // TODO: this is an async call and we're not waiting for it to complete - + // so potentially an event could be fired prior to it completing and would be + // missing the user identification. + getAnalytics().identifyUser(MatrixClientPeg.get().getUserId()); break; case 'reject_cookies': SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false); From f53de5de99d3099564022c79e917dfc80c1af622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 22 Jul 2021 18:04:24 +0200 Subject: [PATCH 024/115] Make bubble layout background color less agressive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/themes/dark/css/_dark.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 655492661c..7b83fe0cb2 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -230,7 +230,7 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28); // Bubble tiles $eventbubble-self-bg: #143A34; $eventbubble-others-bg: #394049; -$eventbubble-bg-hover: #433C23; +$eventbubble-bg-hover: $header-panel-bg-color; $eventbubble-avatar-outline: $bg-color; $eventbubble-reply-color: #C1C6CD; From 6c0e0dc64bbd2d219dff9fae016e5e52c05d2955 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Thu, 22 Jul 2021 21:45:20 -0500 Subject: [PATCH 025/115] Allow all of the URL schemes that Firefox allows Signed-off-by: Aaron Raimist --- src/HtmlUtils.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index af5d2b3019..27556a4012 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -57,7 +57,10 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i'); const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; -export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet', 'matrix']; +export const PERMITTED_URL_SCHEMES = ["bitcoin", "ftp", "geo", "http", "https", "im", "irc", + "ircs", "magnet", "mailto", "matrix", "mms", "news", + "nntp", "openpgp4fpr", "sip", "sftp", "sms", "smsto", + "ssh", "tel", "urn", "webcal", "wtai", "xmpp"]; const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/; From 95f4275807ca3207f179288ae7cbde6352f61167 Mon Sep 17 00:00:00 2001 From: James Salter Date: Fri, 23 Jul 2021 16:47:02 +0100 Subject: [PATCH 026/115] Add Disabled anonymity, improve tests --- src/PosthogAnalytics.ts | 10 ++++---- test/PosthogAnalytics-test.ts | 45 ++++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 9c167f5464..3e757060db 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -12,6 +12,7 @@ interface IEvent { } export enum Anonymity { + Disabled, Anonymous, Pseudonymous } @@ -181,12 +182,12 @@ export class PosthogAnalytics { } private async capture(eventName: string, properties: posthog.Properties) { - if (!this.enabled) { - return; - } if (!this.initialised) { throw Error("Tried to track event before PoshogAnalytics.init has completed"); } + if (!this.enabled) { + return; + } const { origin, hash, pathname } = window.location; properties['$redacted_current_url'] = await getRedactedCurrentLocation( origin, hash, pathname, this.anonymity); @@ -197,7 +198,7 @@ export class PosthogAnalytics { eventName: E["eventName"], properties: E["properties"], ) { - if (this.anonymity == Anonymity.Anonymous) return; + if (this.anonymity == Anonymity.Anonymous || this.anonymity == Anonymity.Disabled) return; await this.capture(eventName, properties); } @@ -205,6 +206,7 @@ export class PosthogAnalytics { eventName: E["eventName"], properties: E["properties"], ) { + if (this.anonymity == Anonymity.Disabled) return; await this.capture(eventName, properties); } diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index a33544e738..cefaafe78f 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -1,4 +1,4 @@ -import { Anonymity, getRedactedCurrentLocation, IAnonymousEvent, IRoomEvent, +import { Anonymity, getRedactedCurrentLocation, IAnonymousEvent, IPseudonymousEvent, IRoomEvent, PosthogAnalytics } from '../src/PosthogAnalytics'; import SdkConfig from '../src/SdkConfig'; const crypto = require('crypto'); @@ -22,6 +22,13 @@ export interface ITestEvent extends IAnonymousEvent { } } +export interface ITestPseudonymousEvent extends IPseudonymousEvent { + key: "jest_test_pseudo_event", + properties: { + foo: string + } +} + export interface ITestRoomEvent extends IRoomEvent { key: "jest_test_room_event", properties: { @@ -75,7 +82,7 @@ describe("PosthogAnalytics", () => { }); }); - it("Should pass track() to posthog", async () => { + it("Should pass trackAnonymousEvent() to posthog", async () => { analytics.init(Anonymity.Pseudonymous); await analytics.trackAnonymousEvent("jest_test_event", { foo: "bar", @@ -96,18 +103,44 @@ describe("PosthogAnalytics", () => { .toEqual("73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049"); }); - it("Should silently not track if not inititalised", async () => { - await analytics.trackAnonymousEvent("jest_test_event", { + it("Should pass trackPseudonymousEvent() to posthog", async () => { + analytics.init(Anonymity.Pseudonymous); + await analytics.trackPseudonymousEvent("jest_test_pseudo_event", { + foo: "bar", + }); + expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_pseudo_event"); + expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar"); + }); + + it("Should blow up if not inititalised prior to tracking", async () => { + const fn = () => { + return analytics.trackAnonymousEvent("jest_test_event", { + foo: "bar", + }); + }; + await expect(fn()).rejects.toThrow(); + }); + + it("Should not track pseudonymous messages if anonymous", async () => { + analytics.init(Anonymity.Anonymous); + await analytics.trackPseudonymousEvent("jest_test_event", { foo: "bar", }); expect(fakePosthog.capture.mock.calls.length).toBe(0); }); - it("Should not track non-anonymous messages if anonymous", async () => { - analytics.init(Anonymity.Anonymous); + it("Should not track any events if disabled", async () => { + analytics.init(Anonymity.Disabled); await analytics.trackPseudonymousEvent("jest_test_event", { foo: "bar", }); + await analytics.trackAnonymousEvent("jest_test_event", { + foo: "bar", + }); + await analytics.trackRoomEvent("room id", "jest_test_room_event", { + foo: "bar", + }); + await analytics.trackPageView(200); expect(fakePosthog.capture.mock.calls.length).toBe(0); }); From 5e0a3976316d267ebedb285c6dd61f7e471dde22 Mon Sep 17 00:00:00 2001 From: James Salter Date: Fri, 23 Jul 2021 17:58:31 +0100 Subject: [PATCH 027/115] Refactor anonymity derivation --- src/PosthogAnalytics.ts | 32 ++++++++++++++++++++++-- src/components/structures/MatrixChat.tsx | 6 +++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 3e757060db..535781cb08 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -1,6 +1,7 @@ import posthog, { PostHog } from 'posthog-js'; import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; +import SettingsStore from './settings/SettingsStore'; interface IEvent { // The event name that will be used by PostHog. @@ -78,10 +79,14 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p export class PosthogAnalytics { private anonymity = Anonymity.Anonymous; - private initialised = false; private posthog?: PostHog = null; + + // set true during init() if posthog config is present private enabled = false; + // set to true after init() has been called + private initialised = false; + private static _instance = null; public static instance(): PosthogAnalytics { @@ -155,7 +160,9 @@ export class PosthogAnalytics { } public registerSuperProperties(properties) { - this.posthog.register(properties); + if (this.enabled) { + this.posthog.register(properties); + } } public isInitialised() { @@ -248,3 +255,24 @@ export async function getPlatformProperties() { export function getAnalytics(): PosthogAnalytics { return PosthogAnalytics.instance(); } + +export function getAnonymityFromSettings(): Anonymity { + // determine the current anonymity level based on curernt user settings + + // "Send anonymous usage data which helps us improve Element. This will use a cookie." + const analyticsOptIn = SettingsStore.getValue("analyticsOptIn"); + + // "Send pseudonymous usage data which helps us improve Element. This will use a cookie." + const pseudonumousOptIn = SettingsStore.getValue("pseudonymousAnalyticsOptIn"); + + let anonymity; + if (pseudonumousOptIn) { + anonymity = Anonymity.Pseudonymous; + } else if (analyticsOptIn) { + anonymity = Anonymity.Anonymous; + } else { + anonymity = Anonymity.Disabled; + } + + return anonymity; +} diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 513200520f..bd54b0ebc9 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -107,7 +107,7 @@ import UIStore, { UI_EVENTS } from "../../stores/UIStore"; import SoftLogout from './auth/SoftLogout'; import { makeRoomPermalink } from "../../utils/permalinks/Permalinks"; import { copyPlaintext } from "../../utils/strings"; -import { Anonymity, getAnalytics, getPlatformProperties } from '../../PosthogAnalytics'; +import { Anonymity, getAnalytics, getAnonymityFromSettings, getPlatformProperties } from '../../PosthogAnalytics'; /** constants for MatrixChat.state.view */ export enum Views { @@ -390,7 +390,9 @@ export default class MatrixChat extends React.PureComponent { } const analytics = getAnalytics(); - analytics.init(SettingsStore.getValue("analyticsOptIn") ? Anonymity.Pseudonymous : Anonymity.Anonymous); + analytics.init(getAnonymityFromSettings()); + // note this requires a network request in the browser, so some events can potentially + // before before registerSuperProperties has been called getPlatformProperties().then((properties) => analytics.registerSuperProperties(properties)); CountlyAnalytics.instance.enable(/* anonymous = */ true); From ff37b8cc79205e57969ed44d867b215a492bbf63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 12:47:30 +0200 Subject: [PATCH 028/115] Remove IncomingCallBox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallContainer.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/voip/CallContainer.tsx b/src/components/views/voip/CallContainer.tsx index fa963e4e28..41046b9952 100644 --- a/src/components/views/voip/CallContainer.tsx +++ b/src/components/views/voip/CallContainer.tsx @@ -1,5 +1,6 @@ /* Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import IncomingCallBox from './IncomingCallBox'; import CallPreview from './CallPreview'; import { replaceableComponent } from "../../../utils/replaceableComponent"; @@ -31,7 +31,6 @@ interface IState { export default class CallContainer extends React.PureComponent { public render() { return
-
; } From 07be6dd78065e325f1f31ce9de51efaa2b5c6388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 13:04:06 +0200 Subject: [PATCH 029/115] Allow suppliing whole body to toasts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/ToastContainer.tsx | 22 +++++++++++++------- src/stores/ToastStore.ts | 4 +++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index b7b0b7c652..75cf4a51fc 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -58,7 +58,7 @@ export default class ToastContainer extends React.Component<{}, IState> { let containerClasses; if (totalCount !== 0) { const topToast = this.state.toasts[0]; - const { title, icon, key, component, className, props } = topToast; + const { title, icon, key, component, className, props, supplyWholeBody } = topToast; const toastClasses = classNames("mx_Toast_toast", { "mx_Toast_hasIcon": icon, [`mx_Toast_icon_${icon}`]: icon, @@ -73,16 +73,22 @@ export default class ToastContainer extends React.Component<{}, IState> { key, toastKey: key, }); - toast = (
-
-

{ title }

- { countIndicator } -
-
{ React.createElement(component, toastProps) }
-
); + + const content = React.createElement(component, toastProps); + + toast = supplyWholeBody + ? content + :
+
+

{ title }

+ { countIndicator } +
+
{ content }
+
; containerClasses = classNames("mx_ToastContainer", { "mx_ToastContainer_stacked": isStacked, + [className]: supplyWholeBody, }); } return toast diff --git a/src/stores/ToastStore.ts b/src/stores/ToastStore.ts index 850c3cb026..e831be7203 100644 --- a/src/stores/ToastStore.ts +++ b/src/stores/ToastStore.ts @@ -22,11 +22,13 @@ export interface IToast { key: string; // higher priority number will be shown on top of lower priority priority: number; - title: string; + title?: string; icon?: string; component: C; className?: string; props?: Omit, "toastKey">; // toastKey is injected by ToastContainer + supplyWholeBody?: boolean; + content?: JSX.Element; } /** From 410928745f41e86451f0f27c944577a82a800170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 13:05:14 +0200 Subject: [PATCH 030/115] IncomingCallBox -> IncomingCallToast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/_components.scss | 1 + res/css/views/toasts/_IncomingCallToast.scss | 100 ++++++++++ res/css/views/voip/_CallContainer.scss | 80 -------- src/CallHandler.tsx | 17 ++ src/components/views/voip/IncomingCallBox.tsx | 176 ------------------ src/toasts/IncomingCallToast.tsx | 139 ++++++++++++++ 6 files changed, 257 insertions(+), 256 deletions(-) create mode 100644 res/css/views/toasts/_IncomingCallToast.scss delete mode 100644 src/components/views/voip/IncomingCallBox.tsx create mode 100644 src/toasts/IncomingCallToast.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index f9e3ab1160..b87b45093c 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -263,6 +263,7 @@ @import "./views/spaces/_SpacePublicShare.scss"; @import "./views/terms/_InlineTermsAgreement.scss"; @import "./views/toasts/_AnalyticsToast.scss"; +@import "./views/toasts/_IncomingCallToast.scss"; @import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/verification/_VerificationShowSas.scss"; @import "./views/voip/_CallContainer.scss"; diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss new file mode 100644 index 0000000000..5ce99bd11e --- /dev/null +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -0,0 +1,100 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_IncomingCallToast { + // mx_Toast overrides + padding: 8px !important; + display: unset !important; + top: 8px !important; + border-radius: 8px; + + min-width: 250px; + box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); + background-color: $voipcall-plinth-color; // To match mx_Toast + + pointer-events: initial; // restore pointer events so the user can accept/decline + cursor: pointer; + + .mx_IncomingCallToast_CallerInfo { + display: flex; + direction: row; + + img, .mx_BaseAvatar_initial { + margin: 8px; + } + + > div { + display: flex; + flex-direction: column; + + justify-content: center; + } + + h1, p { + margin: 0px; + padding: 0px; + font-size: $font-14px; + line-height: $font-16px; + } + + h1 { + font-weight: bold; + } + } + + .mx_IncomingCallToast_buttons { + padding: 8px; + display: flex; + flex-direction: row; + + > .mx_IncomingCallToast_spacer { + width: 8px; + } + + > * { + flex-shrink: 0; + flex-grow: 1; + margin-right: 0; + font-size: $font-15px; + line-height: $font-24px; + } + } + + .mx_IncomingCallToast_iconButton { + position: absolute; + right: 8px; + + &::before { + content: ''; + + height: 20px; + width: 20px; + background-color: $icon-button-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } + } + + .mx_IncomingCallToast_silence::before { + mask-image: url('$(res)/img/voip/silence.svg'); + } + + .mx_IncomingCallToast_unSilence::before { + mask-image: url('$(res)/img/voip/un-silence.svg'); + } +} diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss index 0c09070334..181a5ee0a3 100644 --- a/res/css/views/voip/_CallContainer.scss +++ b/res/css/views/voip/_CallContainer.scss @@ -43,84 +43,4 @@ limitations under the License. .mx_AppTile_persistedWrapper div { min-width: 350px; } - - .mx_IncomingCallBox { - min-width: 250px; - background-color: $voipcall-plinth-color; - padding: 8px; - box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); - border-radius: 8px; - - pointer-events: initial; // restore pointer events so the user can accept/decline - cursor: pointer; - - .mx_IncomingCallBox_CallerInfo { - display: flex; - direction: row; - - img, .mx_BaseAvatar_initial { - margin: 8px; - } - - > div { - display: flex; - flex-direction: column; - - justify-content: center; - } - - h1, p { - margin: 0px; - padding: 0px; - font-size: $font-14px; - line-height: $font-16px; - } - - h1 { - font-weight: bold; - } - } - - .mx_IncomingCallBox_buttons { - padding: 8px; - display: flex; - flex-direction: row; - - > .mx_IncomingCallBox_spacer { - width: 8px; - } - - > * { - flex-shrink: 0; - flex-grow: 1; - margin-right: 0; - font-size: $font-15px; - line-height: $font-24px; - } - } - - .mx_IncomingCallBox_iconButton { - position: absolute; - right: 8px; - - &::before { - content: ''; - - height: 20px; - width: 20px; - background-color: $icon-button-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - } - } - - .mx_IncomingCallBox_silence::before { - mask-image: url('$(res)/img/voip/silence.svg'); - } - - .mx_IncomingCallBox_unSilence::before { - mask-image: url('$(res)/img/voip/un-silence.svg'); - } - } } diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index e7ba1aa9fb..e9831b5315 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -88,6 +88,9 @@ import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/ import EventEmitter from 'events'; import SdkConfig from './SdkConfig'; import { ensureDMExists, findDMForUser } from './createRoom'; +import { getIncomingCallToastKey } from './toasts/IncomingCallToast'; +import ToastStore from './stores/ToastStore'; +import IncomingCallToast from "./toasts/IncomingCallToast"; export const PROTOCOL_PSTN = 'm.protocol.pstn'; export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn'; @@ -641,6 +644,20 @@ export default class CallHandler extends EventEmitter { `Call state in ${mappedRoomId} changed to ${status}`, ); + const toastKey = getIncomingCallToastKey(call.callId); + if (status === CallState.Ringing) { + ToastStore.sharedInstance().addOrReplaceToast({ + key: toastKey, + supplyWholeBody: true, + priority: 100, + component: IncomingCallToast, + className: "mx_IncomingCallToast", + props: { call }, + }); + } else { + ToastStore.sharedInstance().dismissToast(toastKey); + } + dis.dispatch({ action: 'call_state', room_id: mappedRoomId, diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx deleted file mode 100644 index 95e97f1080..0000000000 --- a/src/components/views/voip/IncomingCallBox.tsx +++ /dev/null @@ -1,176 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import dis from '../../../dispatcher/dispatcher'; -import { _t } from '../../../languageHandler'; -import { ActionPayload } from '../../../dispatcher/payloads'; -import CallHandler, { CallHandlerEvent } from '../../../CallHandler'; -import RoomAvatar from '../avatars/RoomAvatar'; -import AccessibleButton from '../elements/AccessibleButton'; -import { CallState } from 'matrix-js-sdk/src/webrtc/call'; -import { replaceableComponent } from "../../../utils/replaceableComponent"; -import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; -import classNames from 'classnames'; - -interface IProps { -} - -interface IState { - incomingCall: any; - silenced: boolean; -} - -@replaceableComponent("views.voip.IncomingCallBox") -export default class IncomingCallBox extends React.Component { - private dispatcherRef: string; - - constructor(props: IProps) { - super(props); - - this.dispatcherRef = dis.register(this.onAction); - this.state = { - incomingCall: null, - silenced: false, - }; - } - - componentDidMount = () => { - CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); - }; - - public componentWillUnmount() { - dis.unregister(this.dispatcherRef); - CallHandler.sharedInstance().removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); - } - - private onAction = (payload: ActionPayload) => { - switch (payload.action) { - case 'call_state': { - const call = CallHandler.sharedInstance().getCallForRoom(payload.room_id); - if (call && call.state === CallState.Ringing) { - this.setState({ - incomingCall: call, - silenced: false, // Reset silenced state for new call - }); - } else { - this.setState({ - incomingCall: null, - }); - } - } - } - }; - - private onSilencedCallsChanged = () => { - const callId = this.state.incomingCall?.callId; - if (!callId) return; - this.setState({ silenced: CallHandler.sharedInstance().isCallSilenced(callId) }); - }; - - private onAnswerClick: React.MouseEventHandler = (e) => { - e.stopPropagation(); - dis.dispatch({ - action: 'answer', - room_id: CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall), - }); - }; - - private onRejectClick: React.MouseEventHandler = (e) => { - e.stopPropagation(); - dis.dispatch({ - action: 'reject', - room_id: CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall), - }); - }; - - private onSilenceClick: React.MouseEventHandler = (e) => { - e.stopPropagation(); - const callId = this.state.incomingCall.callId; - this.state.silenced ? - CallHandler.sharedInstance().unSilenceCall(callId): - CallHandler.sharedInstance().silenceCall(callId); - }; - - public render() { - if (!this.state.incomingCall) { - return null; - } - - let room = null; - if (this.state.incomingCall) { - room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall)); - } - - const caller = room ? room.name : _t("Unknown caller"); - - let incomingCallText = null; - if (this.state.incomingCall) { - if (this.state.incomingCall.type === "voice") { - incomingCallText = _t("Incoming voice call"); - } else if (this.state.incomingCall.type === "video") { - incomingCallText = _t("Incoming video call"); - } else { - incomingCallText = _t("Incoming call"); - } - } - - const silenceClass = classNames({ - "mx_IncomingCallBox_iconButton": true, - "mx_IncomingCallBox_unSilence": this.state.silenced, - "mx_IncomingCallBox_silence": !this.state.silenced, - }); - - return
-
- -
-

{ caller }

-

{ incomingCallText }

-
- -
-
- - { _t("Decline") } - -
- - { _t("Accept") } - -
-
; - } -} diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx new file mode 100644 index 0000000000..2a3e2bd805 --- /dev/null +++ b/src/toasts/IncomingCallToast.tsx @@ -0,0 +1,139 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; +import classNames from 'classnames'; +import { replaceableComponent } from '../utils/replaceableComponent'; +import CallHandler, { CallHandlerEvent } from '../CallHandler'; +import dis from '../dispatcher/dispatcher'; +import { MatrixClientPeg } from '../MatrixClientPeg'; +import { _t } from '../languageHandler'; +import RoomAvatar from '../components/views/avatars/RoomAvatar'; +import AccessibleTooltipButton from '../components/views/elements/AccessibleTooltipButton'; +import AccessibleButton from '../components/views/elements/AccessibleButton'; + +export const getIncomingCallToastKey = (callId: string) => `call_${callId}`; + +interface IProps { + call: MatrixCall; +} + +interface IState { + silenced: boolean; +} + +@replaceableComponent("views.voip.IncomingCallToast") +export default class IncomingCallToast extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + silenced: false, + }; + } + + componentDidMount = () => { + CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); + }; + + public componentWillUnmount() { + CallHandler.sharedInstance().removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); + } + + private onSilencedCallsChanged = () => { + this.setState({ silenced: CallHandler.sharedInstance().isCallSilenced(this.props.call.callId) }); + }; + + private onAnswerClick= (e: React.MouseEvent) => { + e.stopPropagation(); + dis.dispatch({ + action: 'answer', + room_id: CallHandler.sharedInstance().roomIdForCall(this.props.call), + }); + }; + + private onRejectClick= (e: React.MouseEvent) => { + e.stopPropagation(); + dis.dispatch({ + action: 'reject', + room_id: CallHandler.sharedInstance().roomIdForCall(this.props.call), + }); + }; + + private onSilenceClick = (e: React.MouseEvent) => { + e.stopPropagation(); + const callId = this.props.call.callId; + this.state.silenced ? + CallHandler.sharedInstance().unSilenceCall(callId) : + CallHandler.sharedInstance().silenceCall(callId); + }; + + public render() { + const call = this.props.call; + let room = null; + room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(call)); + + const caller = room ? room.name : _t("Unknown caller"); + + const incomingCallText = call.type === CallType.Voice ? _t("Incoming voice call") : _t("Incoming video call"); + + const silenceClass = classNames({ + "mx_IncomingCallToast_iconButton": true, + "mx_IncomingCallToast_unSilence": this.state.silenced, + "mx_IncomingCallToast_silence": !this.state.silenced, + }); + + return +
+ +
+

{ caller }

+

{ incomingCallText }

+
+ +
+
+ + { _t("Decline") } + +
+ + { _t("Accept") } + +
+ ; + } +} From af22588682a8b64da1e929bbccec8fe3c197aa26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 13:39:39 +0200 Subject: [PATCH 031/115] Don't use a spacer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 5 +---- src/toasts/IncomingCallToast.tsx | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index 5ce99bd11e..f7edf6a7bd 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -60,10 +60,7 @@ limitations under the License. padding: 8px; display: flex; flex-direction: row; - - > .mx_IncomingCallToast_spacer { - width: 8px; - } + gap: 12px; > * { flex-shrink: 0; diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index 2a3e2bd805..83cd7aba80 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -125,7 +125,6 @@ export default class IncomingCallToast extends React.Component { > { _t("Decline") } -
Date: Sat, 24 Jul 2021 13:46:06 +0200 Subject: [PATCH 032/115] Correct button sizes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index f7edf6a7bd..02b27a94ab 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -63,6 +63,8 @@ limitations under the License. gap: 12px; > * { + height: 24px; + padding: 0px 12px; flex-shrink: 0; flex-grow: 1; margin-right: 0; From 24e6fc96f6c84b1d1676ee609671abbb3663ddfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 14:04:22 +0200 Subject: [PATCH 033/115] Reorganize content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 22 +++---- src/toasts/IncomingCallToast.tsx | 61 ++++++++++---------- 2 files changed, 36 insertions(+), 47 deletions(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index 02b27a94ab..d2947395dc 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -18,10 +18,12 @@ limitations under the License. .mx_IncomingCallToast { // mx_Toast overrides padding: 8px !important; - display: unset !important; + display: flex !important; top: 8px !important; border-radius: 8px; + flex-direction: row; + gap: 8px; min-width: 250px; box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); background-color: $voipcall-plinth-color; // To match mx_Toast @@ -29,20 +31,9 @@ limitations under the License. pointer-events: initial; // restore pointer events so the user can accept/decline cursor: pointer; - .mx_IncomingCallToast_CallerInfo { + .mx_IncomingCallToast_content { display: flex; - direction: row; - - img, .mx_BaseAvatar_initial { - margin: 8px; - } - - > div { - display: flex; - flex-direction: column; - - justify-content: center; - } + flex-direction: column; h1, p { margin: 0px; @@ -57,7 +48,7 @@ limitations under the License. } .mx_IncomingCallToast_buttons { - padding: 8px; + margin-top: 8px; display: flex; flex-direction: row; gap: 12px; @@ -74,6 +65,7 @@ limitations under the License. } .mx_IncomingCallToast_iconButton { + display: flex; position: absolute; right: 8px; diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index 83cd7aba80..cff0c82782 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -87,12 +87,7 @@ export default class IncomingCallToast extends React.Component { public render() { const call = this.props.call; - let room = null; - room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(call)); - - const caller = room ? room.name : _t("Unknown caller"); - - const incomingCallText = call.type === CallType.Voice ? _t("Incoming voice call") : _t("Incoming video call"); + const room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(call)); const silenceClass = classNames({ "mx_IncomingCallToast_iconButton": true, @@ -101,37 +96,39 @@ export default class IncomingCallToast extends React.Component { }); return -
- -
-

{ caller }

-

{ incomingCallText }

-
+ +
+

+ { room ? room.name : _t("Unknown caller") } +

+

+ { call.type === CallType.Voice ? _t("Incoming voice call") : _t("Incoming video call") } +

-
-
- - { _t("Decline") } - - - { _t("Accept") } - +
+ + { _t("Decline") } + + + { _t("Accept") } + +
; } From dd800549d734231b68aef31e484f56cd25410146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 14:06:52 +0200 Subject: [PATCH 034/115] Fix the silence icon color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index d2947395dc..0a36084f7a 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -74,7 +74,7 @@ limitations under the License. height: 20px; width: 20px; - background-color: $icon-button-color; + background-color: $secondary-fg-color; mask-repeat: no-repeat; mask-size: contain; mask-position: center; From a6f10a4aaa25b91172c6133b484da0df7be7dbba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 18:59:54 +0200 Subject: [PATCH 035/115] Move around some CSS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 66 ++++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index 0a36084f7a..a2d775c969 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -45,47 +45,47 @@ limitations under the License. h1 { font-weight: bold; } - } - .mx_IncomingCallToast_buttons { - margin-top: 8px; - display: flex; - flex-direction: row; - gap: 12px; + .mx_IncomingCallToast_buttons { + margin-top: 8px; + display: flex; + flex-direction: row; + gap: 12px; - > * { - height: 24px; - padding: 0px 12px; - flex-shrink: 0; - flex-grow: 1; - margin-right: 0; - font-size: $font-15px; - line-height: $font-24px; + > * { + height: 24px; + padding: 0px 12px; + flex-shrink: 0; + flex-grow: 1; + margin-right: 0; + font-size: $font-15px; + line-height: $font-24px; + } } - } - .mx_IncomingCallToast_iconButton { - display: flex; - position: absolute; - right: 8px; + .mx_IncomingCallToast_iconButton { + display: flex; + position: absolute; + right: 8px; - &::before { - content: ''; + &::before { + content: ''; - height: 20px; - width: 20px; - background-color: $secondary-fg-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; + height: 20px; + width: 20px; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } } - } - .mx_IncomingCallToast_silence::before { - mask-image: url('$(res)/img/voip/silence.svg'); - } + .mx_IncomingCallToast_silence::before { + mask-image: url('$(res)/img/voip/silence.svg'); + } - .mx_IncomingCallToast_unSilence::before { - mask-image: url('$(res)/img/voip/un-silence.svg'); + .mx_IncomingCallToast_unSilence::before { + mask-image: url('$(res)/img/voip/un-silence.svg'); + } } } From 25d62983de958b16300e88bc6524fb247d053eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 19:12:12 +0200 Subject: [PATCH 036/115] Add button icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 36 +++++++++++++++++++- src/toasts/IncomingCallToast.tsx | 20 ++++++----- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index a2d775c969..665109bc0f 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -46,13 +46,25 @@ limitations under the License. font-weight: bold; } + &.mx_IncomingCallToast_content_voice { + .mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before { + mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); + } + } + + &.mx_IncomingCallToast_content_video { + .mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before { + mask-image: url('$(res)/img/element-icons/call/video-call.svg'); + } + } + .mx_IncomingCallToast_buttons { margin-top: 8px; display: flex; flex-direction: row; gap: 12px; - > * { + .mx_IncomingCallToast_button { height: 24px; padding: 0px 12px; flex-shrink: 0; @@ -60,6 +72,28 @@ limitations under the License. margin-right: 0; font-size: $font-15px; line-height: $font-24px; + + span { + padding: 8px 0; + display: flex; + align-items: center; + + &::before { + content: ''; + display: inline-block; + background-color: $button-fg-color; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 16px; + width: 16px; + height: 16px; + margin-right: 8px; + } + } + + &.mx_IncomingCallToast_button_decline span::before { + mask-image: url('$(res)/img/element-icons/call/hangup.svg'); + } } } diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index cff0c82782..9e5528d6a7 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -88,9 +88,13 @@ export default class IncomingCallToast extends React.Component { public render() { const call = this.props.call; const room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(call)); + const isVoice = call.type === CallType.Voice; - const silenceClass = classNames({ - "mx_IncomingCallToast_iconButton": true, + const contentClass = classNames("mx_IncomingCallToast_content", { + "mx_IncomingCallToast_content_voice": isVoice, + "mx_IncomingCallToast_content_video": !isVoice, + }); + const silenceClass = classNames("mx_IncomingCallToast_iconButton", { "mx_IncomingCallToast_unSilence": this.state.silenced, "mx_IncomingCallToast_silence": !this.state.silenced, }); @@ -101,12 +105,12 @@ export default class IncomingCallToast extends React.Component { height={32} width={32} /> -
+

{ room ? room.name : _t("Unknown caller") }

- { call.type === CallType.Voice ? _t("Incoming voice call") : _t("Incoming video call") } + { isVoice ? _t("Incoming voice call") : _t("Incoming video call") }

{ />
- { _t("Decline") } + { _t("Decline") } - { _t("Accept") } + { _t("Accept") }
From 064544369a033fe3a84734466cca4710db9ac16a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 19:30:37 +0200 Subject: [PATCH 037/115] Add call type icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 27 ++++++++++++++++++-- src/toasts/IncomingCallToast.tsx | 7 ++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index 665109bc0f..bcfb61ed21 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -35,7 +35,7 @@ limitations under the License. display: flex; flex-direction: column; - h1, p { + h1, .mx_CallEvent_type { margin: 0px; padding: 0px; font-size: $font-14px; @@ -46,13 +46,36 @@ limitations under the License. font-weight: bold; } + .mx_CallEvent_type { + display: flex; + flex-direction: row; + + .mx_CallEvent_type_icon { + height: 16px; + width: 16px; + margin-right: 6px; + + &::before { + content: ''; + position: absolute; + height: inherit; + width: inherit; + background-color: $tertiary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + } + } + } + &.mx_IncomingCallToast_content_voice { + .mx_CallEvent_type .mx_CallEvent_type_icon::before, .mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before { mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); } } &.mx_IncomingCallToast_content_video { + .mx_CallEvent_type .mx_CallEvent_type_icon::before, .mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before { mask-image: url('$(res)/img/element-icons/call/video-call.svg'); } @@ -107,7 +130,7 @@ limitations under the License. height: 20px; width: 20px; - background-color: $secondary-fg-color; + background-color: $tertiary-fg-color; mask-repeat: no-repeat; mask-size: contain; mask-position: center; diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index 9e5528d6a7..f8a7f0591f 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -109,9 +109,10 @@ export default class IncomingCallToast extends React.Component {

{ room ? room.name : _t("Unknown caller") }

-

- { isVoice ? _t("Incoming voice call") : _t("Incoming video call") } -

+
+
+ { isVoice ? _t("Voice call") : _t("Video call") } +
Date: Sat, 24 Jul 2021 20:39:44 +0200 Subject: [PATCH 038/115] Move silence button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 43 ++++++++++---------- src/toasts/IncomingCallToast.tsx | 10 ++--- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index bcfb61ed21..04f92bb095 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -29,7 +29,6 @@ limitations under the License. background-color: $voipcall-plinth-color; // To match mx_Toast pointer-events: initial; // restore pointer events so the user can accept/decline - cursor: pointer; .mx_IncomingCallToast_content { display: flex; @@ -119,30 +118,30 @@ limitations under the License. } } } + } - .mx_IncomingCallToast_iconButton { - display: flex; - position: absolute; - right: 8px; + .mx_IncomingCallToast_iconButton { + display: flex; + height: 20px; + width: 20px; - &::before { - content: ''; + &::before { + content: ''; - height: 20px; - width: 20px; - background-color: $tertiary-fg-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - } - } - - .mx_IncomingCallToast_silence::before { - mask-image: url('$(res)/img/voip/silence.svg'); - } - - .mx_IncomingCallToast_unSilence::before { - mask-image: url('$(res)/img/voip/un-silence.svg'); + height: inherit; + width: inherit; + background-color: $tertiary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; } } + + .mx_IncomingCallToast_silence::before { + mask-image: url('$(res)/img/voip/silence.svg'); + } + + .mx_IncomingCallToast_unSilence::before { + mask-image: url('$(res)/img/voip/un-silence.svg'); + } } diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index f8a7f0591f..7a7aacac12 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -113,11 +113,6 @@ export default class IncomingCallToast extends React.Component {
{ isVoice ? _t("Voice call") : _t("Video call") }
-
{
+ ; } } From 1f9cd79bcfe4d5b3aeb024e72d316fa93297dbac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 20:59:15 +0200 Subject: [PATCH 039/115] Fix some spacing and other tiny things MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 37 +++++++++++++------- src/toasts/IncomingCallToast.tsx | 4 +-- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index 04f92bb095..6bdbcdb8b0 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -23,7 +23,6 @@ limitations under the License. border-radius: 8px; flex-direction: row; - gap: 8px; min-width: 250px; box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); background-color: $voipcall-plinth-color; // To match mx_Toast @@ -33,21 +32,27 @@ limitations under the License. .mx_IncomingCallToast_content { display: flex; flex-direction: column; + margin-left: 8px; - h1, .mx_CallEvent_type { - margin: 0px; - padding: 0px; - font-size: $font-14px; - line-height: $font-16px; - } - - h1 { + .mx_CallEvent_caller { font-weight: bold; + font-size: $font-15px; + line-height: $font-18px; + + margin-top: 2px; } .mx_CallEvent_type { + font-size: $font-12px; + line-height: $font-15px; + color: $tertiary-fg-color; + + margin-top: 4px; + margin-bottom: 6px; + display: flex; flex-direction: row; + align-items: center; .mx_CallEvent_type_icon { height: 16px; @@ -88,7 +93,7 @@ limitations under the License. .mx_IncomingCallToast_button { height: 24px; - padding: 0px 12px; + padding: 0px 8px; flex-shrink: 0; flex-grow: 1; margin-right: 0; @@ -106,15 +111,21 @@ limitations under the License. background-color: $button-fg-color; mask-position: center; mask-repeat: no-repeat; - mask-size: 16px; - width: 16px; - height: 16px; margin-right: 8px; } } + &.mx_IncomingCallToast_button_accept span::before { + mask-size: 13px; + width: 13px; + height: 13px; + } + &.mx_IncomingCallToast_button_decline span::before { mask-image: url('$(res)/img/element-icons/call/hangup.svg'); + mask-size: 16px; + width: 16px; + height: 16px; } } } diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index 7a7aacac12..8d14fbd883 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -106,9 +106,9 @@ export default class IncomingCallToast extends React.Component { width={32} />
-

+ { room ? room.name : _t("Unknown caller") } -

+
{ isVoice ? _t("Voice call") : _t("Video call") } From 85095df4b9663fd1d078b57efe01a6fa47f4d2b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 21:03:26 +0200 Subject: [PATCH 040/115] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b36910b41b..46600c48f2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -734,6 +734,13 @@ "Notifications": "Notifications", "Enable desktop notifications": "Enable desktop notifications", "Enable": "Enable", + "Unknown caller": "Unknown caller", + "Voice call": "Voice call", + "Video call": "Video call", + "Decline": "Decline", + "Accept": "Accept", + "Sound on": "Sound on", + "Silence call": "Silence call", "Use app for a better experience": "Use app for a better experience", "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.", "Use app": "Use app", @@ -911,14 +918,6 @@ "Fill Screen": "Fill Screen", "Return to call": "Return to call", "%(name)s on hold": "%(name)s on hold", - "Unknown caller": "Unknown caller", - "Incoming voice call": "Incoming voice call", - "Incoming video call": "Incoming video call", - "Incoming call": "Incoming call", - "Sound on": "Sound on", - "Silence call": "Silence call", - "Decline": "Decline", - "Accept": "Accept", "The other party cancelled the verification.": "The other party cancelled the verification.", "Verified!": "Verified!", "You've successfully verified this user.": "You've successfully verified this user.", @@ -1580,8 +1579,6 @@ "Hide Widgets": "Hide Widgets", "Show Widgets": "Show Widgets", "Search": "Search", - "Voice call": "Voice call", - "Video call": "Video call", "Invites": "Invites", "Favourites": "Favourites", "People": "People", From 379101e3ff3e0b77bcf5c215292e649d9bd4c084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 21:09:00 +0200 Subject: [PATCH 041/115] Remove an unused member MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/ToastStore.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stores/ToastStore.ts b/src/stores/ToastStore.ts index e831be7203..093ea9fb6b 100644 --- a/src/stores/ToastStore.ts +++ b/src/stores/ToastStore.ts @@ -28,7 +28,6 @@ export interface IToast { className?: string; props?: Omit, "toastKey">; // toastKey is injected by ToastContainer supplyWholeBody?: boolean; - content?: JSX.Element; } /** From f2204aa1ffdb16de1592778409ab909fad82c52c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Jul 2021 21:12:29 +0200 Subject: [PATCH 042/115] Remove nonsense comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index 6bdbcdb8b0..d49014efdb 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -20,12 +20,12 @@ limitations under the License. padding: 8px !important; display: flex !important; top: 8px !important; - border-radius: 8px; + border-radius: 8px; flex-direction: row; min-width: 250px; box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); - background-color: $voipcall-plinth-color; // To match mx_Toast + background-color: $voipcall-plinth-color; pointer-events: initial; // restore pointer events so the user can accept/decline From 3287d51b179096a29b3f45f62458bad25c86d0c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 25 Jul 2021 08:06:11 +0200 Subject: [PATCH 043/115] Add some return types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/toasts/IncomingCallToast.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index 8d14fbd883..c842581f9a 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -53,15 +53,15 @@ export default class IncomingCallToast extends React.Component { CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); }; - public componentWillUnmount() { + public componentWillUnmount(): void { CallHandler.sharedInstance().removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); } - private onSilencedCallsChanged = () => { + private onSilencedCallsChanged = (): void => { this.setState({ silenced: CallHandler.sharedInstance().isCallSilenced(this.props.call.callId) }); }; - private onAnswerClick= (e: React.MouseEvent) => { + private onAnswerClick= (e: React.MouseEvent): void => { e.stopPropagation(); dis.dispatch({ action: 'answer', @@ -69,7 +69,7 @@ export default class IncomingCallToast extends React.Component { }); }; - private onRejectClick= (e: React.MouseEvent) => { + private onRejectClick= (e: React.MouseEvent): void => { e.stopPropagation(); dis.dispatch({ action: 'reject', @@ -77,7 +77,7 @@ export default class IncomingCallToast extends React.Component { }); }; - private onSilenceClick = (e: React.MouseEvent) => { + private onSilenceClick = (e: React.MouseEvent): void => { e.stopPropagation(); const callId = this.props.call.callId; this.state.silenced ? From 1d629f2557a7d618355436f99c31cc6b9ce856f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 10:34:59 +0200 Subject: [PATCH 044/115] More TS Co-authored-by: Germain --- src/toasts/IncomingCallToast.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index c842581f9a..a853e1652a 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -49,7 +49,7 @@ export default class IncomingCallToast extends React.Component { }; } - componentDidMount = () => { + public componentDidMount = (): void => { CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged); }; From 40947a2a681f03b62013c9c002b1e4ddea7916db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 12:21:58 +0200 Subject: [PATCH 045/115] Simplifie toast handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/toasts/_IncomingCallToast.scss | 11 +----- src/CallHandler.tsx | 3 +- src/components/structures/ToastContainer.tsx | 37 +++++++++++--------- src/stores/ToastStore.ts | 2 +- 4 files changed, 24 insertions(+), 29 deletions(-) diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index d49014efdb..975628f948 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -16,17 +16,8 @@ limitations under the License. */ .mx_IncomingCallToast { - // mx_Toast overrides - padding: 8px !important; - display: flex !important; - top: 8px !important; - - border-radius: 8px; + display: flex; flex-direction: row; - min-width: 250px; - box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); - background-color: $voipcall-plinth-color; - pointer-events: initial; // restore pointer events so the user can accept/decline .mx_IncomingCallToast_content { diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index e9831b5315..5018c44488 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -648,10 +648,9 @@ export default class CallHandler extends EventEmitter { if (status === CallState.Ringing) { ToastStore.sharedInstance().addOrReplaceToast({ key: toastKey, - supplyWholeBody: true, priority: 100, component: IncomingCallToast, - className: "mx_IncomingCallToast", + bodyClassName: "mx_IncomingCallToast", props: { call }, }); } else { diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index 75cf4a51fc..0b0e871975 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -58,37 +58,42 @@ export default class ToastContainer extends React.Component<{}, IState> { let containerClasses; if (totalCount !== 0) { const topToast = this.state.toasts[0]; - const { title, icon, key, component, className, props, supplyWholeBody } = topToast; - const toastClasses = classNames("mx_Toast_toast", { + const { title, icon, key, component, className, bodyClassName, props } = topToast; + const bodyClasses = classNames("mx_Toast_body", bodyClassName); + const toastClasses = classNames("mx_Toast_toast", className, { "mx_Toast_hasIcon": icon, [`mx_Toast_icon_${icon}`]: icon, - }, className); - - let countIndicator; - if (isStacked || this.state.countSeen > 0) { - countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`; - } - + }); const toastProps = Object.assign({}, props, { key, toastKey: key, }); - const content = React.createElement(component, toastProps); - toast = supplyWholeBody - ? content - :
+ let countIndicator; + if (title && isStacked || this.state.countSeen > 0) { + countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`; + } + + let titleElement; + if (title) { + titleElement = (

{ title }

{ countIndicator }
-
{ content }
-
; + ); + } + + toast = ( +
+ { titleElement } +
{ content }
+
+ ); containerClasses = classNames("mx_ToastContainer", { "mx_ToastContainer_stacked": isStacked, - [className]: supplyWholeBody, }); } return toast diff --git a/src/stores/ToastStore.ts b/src/stores/ToastStore.ts index 093ea9fb6b..5e51de3e26 100644 --- a/src/stores/ToastStore.ts +++ b/src/stores/ToastStore.ts @@ -26,8 +26,8 @@ export interface IToast { icon?: string; component: C; className?: string; + bodyClassName?: string; props?: Omit, "toastKey">; // toastKey is injected by ToastContainer - supplyWholeBody?: boolean; } /** From 277fdf1711186390ee3e6c119da1574d830888aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 12:42:18 +0200 Subject: [PATCH 046/115] voipcall-plinth-color -> quinary-content-color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/structures/_ToastContainer.scss | 4 ++-- res/css/views/voip/_CallView.scss | 2 +- res/themes/dark/css/_dark.scss | 2 +- res/themes/legacy-dark/css/_legacy-dark.scss | 2 +- res/themes/legacy-light/css/_legacy-light.scss | 8 ++++---- res/themes/light/css/_light.scss | 8 ++++---- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index d248568740..b6034be42d 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -28,7 +28,7 @@ limitations under the License. margin: 0 4px; grid-row: 2 / 4; grid-column: 1; - background-color: $dark-panel-bg-color; + background-color: $quinary-content-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); border-radius: 8px; } @@ -37,7 +37,7 @@ limitations under the License. grid-row: 1 / 3; grid-column: 1; color: $primary-fg-color; - background-color: $dark-panel-bg-color; + background-color: $quinary-content-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); border-radius: 8px; overflow: hidden; diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 205d431752..2be4a4b802 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -39,7 +39,7 @@ limitations under the License. .mx_CallView_pip { width: 320px; padding-bottom: 8px; - background-color: $voipcall-plinth-color; + background-color: $quinary-content-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20); border-radius: 8px; diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 655492661c..0907ccdd9a 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -113,7 +113,7 @@ $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #394049; +$quinary-content-color: #394049; // ******************** diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 0c0197cfb0..323fe0651e 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -112,7 +112,7 @@ $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #394049; +$quinary-content-color: #394049; // ******************** diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index b7d45452ff..a4e7af2bb9 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -8,9 +8,9 @@ /* Noto Color Emoji contains digits, in fixed-width, therefore causing digits in flowed text to stand out. TODO: Consider putting all emoji fonts to the end rather than the front. */ -$font-family: Nunito, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Arial, Helvetica, Sans-Serif, 'Noto Color Emoji'; +$font-family: nunito, twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', arial, helvetica, sans-serif, 'Noto Color Emoji'; -$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Courier, monospace, 'Noto Color Emoji'; +$monospace-font-family: inconsolata, twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', courier, monospace, 'Noto Color Emoji'; // unified palette // try to use these colors when possible @@ -179,7 +179,7 @@ $composer-e2e-icon-color: #91a1c0; $header-divider-color: #91a1c0; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #F4F6FA; +$quinary-content-color: #F4F6FA; // ******************** @@ -390,7 +390,7 @@ $eventbubble-reply-color: #C1C6CD; @define-mixin mx_DialogButton_secondary { // flip colours for the secondary ones font-weight: 600; - border: 1px solid $accent-color ! important; + border: 1px solid $accent-color !important; color: $accent-color; background-color: $button-secondary-bg-color; } diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 32722515d8..2f81cb3407 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -8,9 +8,9 @@ /* Noto Color Emoji contains digits, in fixed-width, therefore causing digits in flowed text to stand out. TODO: Consider putting all emoji fonts to the end rather than the front. */ -$font-family: Inter, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Arial, Helvetica, Sans-Serif, 'Noto Color Emoji'; +$font-family: inter, twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', arial, helvetica, sans-serif, 'Noto Color Emoji'; -$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Courier, monospace, 'Noto Color Emoji'; +$monospace-font-family: inconsolata, twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', courier, monospace, 'Noto Color Emoji'; // unified palette // try to use these colors when possible @@ -168,7 +168,7 @@ $composer-e2e-icon-color: #91A1C0; $header-divider-color: #91A1C0; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #F4F6FA; +$quinary-content-color: #F4F6FA; // ******************** @@ -392,7 +392,7 @@ $eventbubble-reply-color: #C1C6CD; @define-mixin mx_DialogButton_secondary { // flip colours for the secondary ones font-weight: 600; - border: 1px solid $accent-color ! important; + border: 1px solid $accent-color !important; color: $accent-color; background-color: $button-secondary-bg-color; } From 7ed5dee74bd57deb9de7e20a3b7813f1f94a07d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Jul 2021 13:17:06 +0200 Subject: [PATCH 047/115] Make colors a bit clearer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/structures/_ToastContainer.scss | 4 ++-- res/css/views/voip/_CallView.scss | 2 +- res/themes/dark/css/_dark.scss | 2 +- res/themes/legacy-dark/css/_legacy-dark.scss | 2 +- res/themes/legacy-light/css/_legacy-light.scss | 4 ++-- res/themes/light/css/_light.scss | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index b6034be42d..2c3f1c705c 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -28,7 +28,7 @@ limitations under the License. margin: 0 4px; grid-row: 2 / 4; grid-column: 1; - background-color: $quinary-content-color; + background-color: $toast-bg-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); border-radius: 8px; } @@ -37,7 +37,7 @@ limitations under the License. grid-row: 1 / 3; grid-column: 1; color: $primary-fg-color; - background-color: $quinary-content-color; + background-color: $toast-bg-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); border-radius: 8px; overflow: hidden; diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 2be4a4b802..8299ad8f9a 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -39,7 +39,7 @@ limitations under the License. .mx_CallView_pip { width: 320px; padding-bottom: 8px; - background-color: $quinary-content-color; + background-color: $toast-bg-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20); border-radius: 8px; diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 0907ccdd9a..6e8d64b807 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -112,8 +112,8 @@ $eventtile-meta-color: $roomtopic-color; $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; -// this probably shouldn't have it's own colour $quinary-content-color: #394049; +$toast-bg-color: $quinary-content-color; // ******************** diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 323fe0651e..064b532bb0 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -111,8 +111,8 @@ $eventtile-meta-color: $roomtopic-color; $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; -// this probably shouldn't have it's own colour $quinary-content-color: #394049; +$toast-bg-color: $quinary-content-color; // ******************** diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index a4e7af2bb9..cbf3d6d1b6 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -178,8 +178,8 @@ $eventtile-meta-color: $roomtopic-color; $composer-e2e-icon-color: #91a1c0; $header-divider-color: #91a1c0; -// this probably shouldn't have it's own colour -$quinary-content-color: #F4F6FA; +$system-light-color: #F4F6FA; +$toast-bg-color: $system-light-color; // ******************** diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 2f81cb3407..1d786383f3 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -167,8 +167,8 @@ $eventtile-meta-color: $roomtopic-color; $composer-e2e-icon-color: #91A1C0; $header-divider-color: #91A1C0; -// this probably shouldn't have it's own colour -$quinary-content-color: #F4F6FA; +$system-light-color: #F4F6FA; +$toast-bg-color: $system-light-color; // ******************** From 5dd34de5fe89504ea1354d3e81c3a23cefcc3805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 27 Jul 2021 14:31:42 +0200 Subject: [PATCH 048/115] Handle mute state changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/VideoFeed.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 95cc5ee3e3..3d873cef0a 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -85,10 +85,12 @@ export default class VideoFeed extends React.Component { if (oldFeed) { this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream); + this.props.feed.removeListener(CallFeedEvent.MuteStateChanged, this.onMuteStateChanged); this.stopMedia(); } if (newFeed) { this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream); + this.props.feed.addListener(CallFeedEvent.MuteStateChanged, this.onMuteStateChanged); this.playMedia(); } } @@ -137,6 +139,14 @@ export default class VideoFeed extends React.Component { this.playMedia(); }; + private onMuteStateChanged = () => { + this.setState({ + audioMuted: this.props.feed.isAudioMuted(), + videoMuted: this.props.feed.isVideoMuted(), + }); + this.playMedia(); + }; + private onResize = (e) => { if (this.props.onResize && !this.props.feed.isLocal()) { this.props.onResize(e); From 474561600e4e04cf112e367e1b3e1c1b8937a956 Mon Sep 17 00:00:00 2001 From: James Salter Date: Tue, 27 Jul 2021 13:29:56 +0100 Subject: [PATCH 049/115] Fix hash == "" --- src/PosthogAnalytics.ts | 23 ++++++++++++++--------- test/PosthogAnalytics-test.ts | 6 ++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 535781cb08..cdb23e582c 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -63,17 +63,22 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p pathname = "//"; } - let [_, screen, ...parts] = hash.split("/"); + let hashStr; + if (hash == "") { + hashStr = ""; + } else { + let [_, screen, ...parts] = hash.split("/"); - if (!knownScreens.has(screen)) { - screen = ""; + if (!knownScreens.has(screen)) { + screen = ""; + } + + for (let i = 0; i < parts.length; i++) { + parts[i] = anonymity === Anonymity.Anonymous ? `` : await hashHex(parts[i]); + } + + hashStr = `${_}/${screen}/${parts.join("/")}`; } - - for (let i = 0; i < parts.length; i++) { - parts[i] = anonymity === Anonymity.Anonymous ? `` : await hashHex(parts[i]); - } - - const hashStr = `${_}/${screen}/${parts.join("/")}`; return origin + pathname + hashStr; } diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index cefaafe78f..7d81b6e86d 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -174,6 +174,12 @@ bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`); expect(location).toBe("https://foo.bar/#///"); }); + it("Should currently handle an empty hash", async () => { + const location = await getRedactedCurrentLocation( + "https://foo.bar", "", "/", Anonymity.Anonymous); + expect(location).toBe("https://foo.bar/"); + }); + it("Should identify the user to posthog if pseudonymous", async () => { analytics.init(Anonymity.Pseudonymous); await analytics.identifyUser("foo"); From 1d81bdc6f9a676d075e6ad83b055b4ee43080a86 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 09:37:08 +0100 Subject: [PATCH 050/115] Interface changes and anonymity fixes --- src/Lifecycle.ts | 10 +- src/PosthogAnalytics.ts | 118 +++++++++--------- src/components/structures/MatrixChat.tsx | 13 +- .../tabs/user/SecurityUserSettingsTab.js | 4 +- src/settings/Settings.tsx | 8 ++ .../PseudonymousAnalyticsController.ts | 26 ++++ test/PosthogAnalytics-test.ts | 44 ++++--- 7 files changed, 124 insertions(+), 99 deletions(-) create mode 100644 src/settings/controllers/PseudonymousAnalyticsController.ts diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index c27c774cd7..b0a521d886 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -48,7 +48,7 @@ import { Jitsi } from "./widgets/Jitsi"; import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY } from "./BasePlatform"; import ThreepidInviteStore from "./stores/ThreepidInviteStore"; import CountlyAnalytics from "./CountlyAnalytics"; -import { Anonymity, getAnalytics, getPlatformProperties } from "./PosthogAnalytics"; +import { getAnalytics } from "./PosthogAnalytics"; import CallHandler from './CallHandler'; import LifecycleCustomisations from "./customisations/Lifecycle"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; @@ -574,13 +574,7 @@ async function doSetLoggedIn( await abortLogin(); } - if (SettingsStore.getValue("analyticsOptIn")) { - const analytics = getAnalytics(); - analytics.setAnonymity(Anonymity.Pseudonymous); - await analytics.identifyUser(credentials.userId); - } else { - getAnalytics().setAnonymity(Anonymity.Anonymous); - } + getAnalytics().updateAnonymityFromSettings(credentials.userId); Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl); diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index cdb23e582c..fa530b5309 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -85,14 +85,10 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p export class PosthogAnalytics { private anonymity = Anonymity.Anonymous; private posthog?: PostHog = null; - - // set true during init() if posthog config is present + // set true during the constructor if posthog config is present, otherwise false private enabled = false; - - // set to true after init() has been called - private initialised = false; - private static _instance = null; + private platformSuperProperties = {}; public static instance(): PosthogAnalytics { if (!this._instance) { @@ -103,10 +99,6 @@ export class PosthogAnalytics { constructor(posthog: PostHog) { this.posthog = posthog; - } - - public init(anonymity: Anonymity) { - this.anonymity = anonymity; const posthogConfig = SdkConfig.get()["posthog"]; if (posthogConfig) { this.posthog.init(posthogConfig.projectApiKey, { @@ -123,7 +115,6 @@ export class PosthogAnalytics { sanitize_properties: this.sanitizeProperties.bind(this), respect_dnt: true, }); - this.initialised = true; this.enabled = true; } else { this.enabled = false; @@ -159,19 +150,39 @@ export class PosthogAnalytics { return properties; } - public async identifyUser(userId: string) { - if (this.anonymity == Anonymity.Anonymous) return; - this.posthog.identify(await hashHex(userId)); + private static getAnonymityFromSettings(): Anonymity { + // determine the current anonymity level based on curernt user settings + + // "Send anonymous usage data which helps us improve Element. This will use a cookie." + const analyticsOptIn = SettingsStore.getValue("analyticsOptIn"); + + // "Send pseudonymous usage data which helps us improve Element. This will use a cookie." + // + // Currently, this is only a labs flag, for testing purposes. + const pseudonumousOptIn = SettingsStore.getValue("feature_pseudonymousAnalyticsOptIn"); + + let anonymity; + if (pseudonumousOptIn) { + anonymity = Anonymity.Pseudonymous; + } else if (analyticsOptIn) { + anonymity = Anonymity.Anonymous; + } else { + anonymity = Anonymity.Disabled; + } + + return anonymity; } - public registerSuperProperties(properties) { - if (this.enabled) { - this.posthog.register(properties); + public async identifyUser(userId: string) { + if (this.anonymity == Anonymity.Pseudonymous) { + this.posthog.identify(await hashHex(userId)); } } - public isInitialised() { - return this.initialised; + private registerSuperProperties(properties) { + if (this.enabled) { + this.posthog.register(properties); + } } public isEnabled() { @@ -179,6 +190,13 @@ export class PosthogAnalytics { } public setAnonymity(anonymity: Anonymity) { + if (this.enabled && (anonymity == Anonymity.Disabled || anonymity == Anonymity.Anonymous)) { + // when transitioning to Disabled or Anonymous ensure we clear out any prior state + // set in posthog e.g. distinct ID + this.posthog.reset(); + // Restore any previously set platform super properties + this.registerSuperProperties(this.platformSuperProperties); + } this.anonymity = anonymity; } @@ -194,9 +212,6 @@ export class PosthogAnalytics { } private async capture(eventName: string, properties: posthog.Properties) { - if (!this.initialised) { - throw Error("Tried to track event before PoshogAnalytics.init has completed"); - } if (!this.enabled) { return; } @@ -239,45 +254,36 @@ export class PosthogAnalytics { durationMs, }); } -} -export async function getPlatformProperties() { - const platform = PlatformPeg.get(); - let appVersion; - try { - appVersion = await platform.getAppVersion(); - } catch (e) { - // this happens if no version is set i.e. in dev - appVersion = "unknown"; + public async updatePlatformSuperProperties() { + this.platformSuperProperties = await PosthogAnalytics.getPlatformProperties(); + this.registerSuperProperties(this.platformSuperProperties); } - return { - appVersion, - appPlatform: platform.getHumanReadableName(), - }; + private static async getPlatformProperties() { + const platform = PlatformPeg.get(); + let appVersion; + try { + appVersion = await platform.getAppVersion(); + } catch (e) { + // this happens if no version is set i.e. in dev + appVersion = "unknown"; + } + + return { + appVersion, + appPlatform: platform.getHumanReadableName(), + }; + } + + public async updateAnonymityFromSettings(userId?: string) { + this.setAnonymity(PosthogAnalytics.getAnonymityFromSettings()); + if (userId && this.getAnonymity() == Anonymity.Pseudonymous) { + await this.identifyUser(userId); + } + } } export function getAnalytics(): PosthogAnalytics { return PosthogAnalytics.instance(); } - -export function getAnonymityFromSettings(): Anonymity { - // determine the current anonymity level based on curernt user settings - - // "Send anonymous usage data which helps us improve Element. This will use a cookie." - const analyticsOptIn = SettingsStore.getValue("analyticsOptIn"); - - // "Send pseudonymous usage data which helps us improve Element. This will use a cookie." - const pseudonumousOptIn = SettingsStore.getValue("pseudonymousAnalyticsOptIn"); - - let anonymity; - if (pseudonumousOptIn) { - anonymity = Anonymity.Pseudonymous; - } else if (analyticsOptIn) { - anonymity = Anonymity.Anonymous; - } else { - anonymity = Anonymity.Disabled; - } - - return anonymity; -} diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index bd54b0ebc9..1a477970fa 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -107,7 +107,7 @@ import UIStore, { UI_EVENTS } from "../../stores/UIStore"; import SoftLogout from './auth/SoftLogout'; import { makeRoomPermalink } from "../../utils/permalinks/Permalinks"; import { copyPlaintext } from "../../utils/strings"; -import { Anonymity, getAnalytics, getAnonymityFromSettings, getPlatformProperties } from '../../PosthogAnalytics'; +import { getAnalytics } from '../../PosthogAnalytics'; /** constants for MatrixChat.state.view */ export enum Views { @@ -390,10 +390,8 @@ export default class MatrixChat extends React.PureComponent { } const analytics = getAnalytics(); - analytics.init(getAnonymityFromSettings()); - // note this requires a network request in the browser, so some events can potentially - // before before registerSuperProperties has been called - getPlatformProperties().then((properties) => analytics.registerSuperProperties(properties)); + analytics.updateAnonymityFromSettings(); + analytics.updatePlatformSuperProperties(); CountlyAnalytics.instance.enable(/* anonymous = */ true); } @@ -831,11 +829,6 @@ export default class MatrixChat extends React.PureComponent { if (CountlyAnalytics.instance.canEnable()) { CountlyAnalytics.instance.enable(/* anonymous = */ false); } - getAnalytics().setAnonymity(Anonymity.Pseudonymous); - // TODO: this is an async call and we're not waiting for it to complete - - // so potentially an event could be fired prior to it completing and would be - // missing the user identification. - getAnalytics().identifyUser(MatrixClientPeg.get().getUserId()); break; case 'reject_cookies': SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false); diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index 15b4992cd8..670e2ec757 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -36,7 +36,7 @@ import { UIFeature } from "../../../../../settings/UIFeature"; import { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel"; import CountlyAnalytics from "../../../../../CountlyAnalytics"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; -import { Anonymity, getAnalytics } from "../../../../../PosthogAnalytics"; +import { getAnalytics } from "../../../../../PosthogAnalytics"; export class IgnoredUser extends React.Component { static propTypes = { @@ -107,7 +107,7 @@ export default class SecurityUserSettingsTab extends React.Component { _updateAnalytics = (checked) => { checked ? Analytics.enable() : Analytics.disable(); CountlyAnalytics.instance.enable(/* anonymous = */ !checked); - getAnalytics().setAnonymity(checked ? Anonymity.Pseudonymous : Anonymity.Anonymous); + getAnalytics().updateAnonymityFromSettings(MatrixClientPeg.get().getUserId()); }; _onExportE2eKeysClicked = () => { diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 830ea9e32e..db0cb05c9f 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -41,6 +41,7 @@ import { Layout } from "./Layout"; import ReducedMotionController from './controllers/ReducedMotionController'; import IncompatibleController from "./controllers/IncompatibleController"; import SdkConfig from "../SdkConfig"; +import PseudonymousAnalyticsController from './controllers/PseudonymousAnalyticsController'; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = [ @@ -297,6 +298,13 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_pseudonymousAnalyticsOptIn": { + isFeature: true, + supportedLevels: LEVELS_FEATURE, + displayName: _td('Send pseudonymous analytics data'), + default: false, + controller: new PseudonymousAnalyticsController(), + }, "advancedRoomListLogging": { // TODO: Remove flag before launch: https://github.com/vector-im/element-web/issues/14231 displayName: _td("Enable advanced debugging for the room list"), diff --git a/src/settings/controllers/PseudonymousAnalyticsController.ts b/src/settings/controllers/PseudonymousAnalyticsController.ts new file mode 100644 index 0000000000..d55efe3c74 --- /dev/null +++ b/src/settings/controllers/PseudonymousAnalyticsController.ts @@ -0,0 +1,26 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import SettingController from "./SettingController"; +import { SettingLevel } from "../SettingLevel"; +import { getAnalytics } from "../../PosthogAnalytics"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; + +export default class PseudonymousAnalyticsController extends SettingController { + public onChange(level: SettingLevel, roomId: string, newValue: any) { + getAnalytics().updateAnonymityFromSettings(MatrixClientPeg.get().getUserId()); + } +} diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index 7d81b6e86d..f726fe0b13 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -7,11 +7,15 @@ class FakePosthog { public capture; public init; public identify; + public reset; + public register; constructor() { this.capture = jest.fn(); this.init = jest.fn(); this.identify = jest.fn(); + this.reset = jest.fn(); + this.register = jest.fn(); } } @@ -37,12 +41,11 @@ export interface ITestRoomEvent extends IRoomEvent { } describe("PosthogAnalytics", () => { - let analytics: PosthogAnalytics; let fakePosthog: FakePosthog; beforeEach(() => { fakePosthog = new FakePosthog(); - analytics = new PosthogAnalytics(fakePosthog); + window.crypto = { subtle: crypto.webcrypto.subtle, }; @@ -53,26 +56,28 @@ describe("PosthogAnalytics", () => { }); describe("Initialisation", () => { - it("Should not initialise if config is not set", async () => { + it("Should not be enabled without config being set", () => { jest.spyOn(SdkConfig, "get").mockReturnValue({}); - analytics.init(Anonymity.Pseudonymous); + const analytics = new PosthogAnalytics(fakePosthog); expect(analytics.isEnabled()).toBe(false); }); - it("Should initialise if config is set", async () => { + it("Should be enabled if config is set", () => { jest.spyOn(SdkConfig, "get").mockReturnValue({ posthog: { projectApiKey: "foo", apiHost: "bar", }, }); - analytics.init(Anonymity.Pseudonymous); - expect(analytics.isInitialised()).toBe(true); + const analytics = new PosthogAnalytics(fakePosthog); + analytics.setAnonymity(Anonymity.Pseudonymous); expect(analytics.isEnabled()).toBe(true); }); }); describe("Tracking", () => { + let analytics: PosthogAnalytics; + beforeEach(() => { jest.spyOn(SdkConfig, "get").mockReturnValue({ posthog: { @@ -80,10 +85,12 @@ describe("PosthogAnalytics", () => { apiHost: "bar", }, }); + + analytics = new PosthogAnalytics(fakePosthog); }); it("Should pass trackAnonymousEvent() to posthog", async () => { - analytics.init(Anonymity.Pseudonymous); + analytics.setAnonymity(Anonymity.Pseudonymous); await analytics.trackAnonymousEvent("jest_test_event", { foo: "bar", }); @@ -92,7 +99,7 @@ describe("PosthogAnalytics", () => { }); it("Should pass trackRoomEvent to posthog", async () => { - analytics.init(Anonymity.Pseudonymous); + analytics.setAnonymity(Anonymity.Pseudonymous); const roomId = "42"; await analytics.trackRoomEvent("jest_test_event", roomId, { foo: "bar", @@ -104,7 +111,7 @@ describe("PosthogAnalytics", () => { }); it("Should pass trackPseudonymousEvent() to posthog", async () => { - analytics.init(Anonymity.Pseudonymous); + analytics.setAnonymity(Anonymity.Pseudonymous); await analytics.trackPseudonymousEvent("jest_test_pseudo_event", { foo: "bar", }); @@ -112,17 +119,8 @@ describe("PosthogAnalytics", () => { expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar"); }); - it("Should blow up if not inititalised prior to tracking", async () => { - const fn = () => { - return analytics.trackAnonymousEvent("jest_test_event", { - foo: "bar", - }); - }; - await expect(fn()).rejects.toThrow(); - }); - it("Should not track pseudonymous messages if anonymous", async () => { - analytics.init(Anonymity.Anonymous); + analytics.setAnonymity(Anonymity.Anonymous); await analytics.trackPseudonymousEvent("jest_test_event", { foo: "bar", }); @@ -130,7 +128,7 @@ describe("PosthogAnalytics", () => { }); it("Should not track any events if disabled", async () => { - analytics.init(Anonymity.Disabled); + analytics.setAnonymity(Anonymity.Disabled); await analytics.trackPseudonymousEvent("jest_test_event", { foo: "bar", }); @@ -181,14 +179,14 @@ bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`); }); it("Should identify the user to posthog if pseudonymous", async () => { - analytics.init(Anonymity.Pseudonymous); + analytics.setAnonymity(Anonymity.Pseudonymous); await analytics.identifyUser("foo"); expect(fakePosthog.identify.mock.calls[0][0]) .toBe("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"); }); it("Should not identify the user to posthog if anonymous", async () => { - analytics.init(Anonymity.Anonymous); + analytics.setAnonymity(Anonymity.Anonymous); await analytics.identifyUser("foo"); expect(fakePosthog.identify.mock.calls.length).toBe(0); }); From a6df687196916cb3b31a4fa0cc52cbebed1bb939 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 09:54:37 +0100 Subject: [PATCH 051/115] Tidy up interface and add some comments --- src/PosthogAnalytics.ts | 102 ++++++++++++++++++++++------------ test/PosthogAnalytics-test.ts | 2 +- 2 files changed, 66 insertions(+), 38 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index fa530b5309..6329598685 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -20,11 +20,12 @@ export enum Anonymity { // If an event extends IPseudonymousEvent, the event contains pseudonymous data // that won't be sent unless the user has explicitly consented to pseudonymous tracking. -// For example, hashed user IDs or room IDs. +// For example, it might contain hashed user IDs or room IDs. +// Such events will be automatically dropped if PosthogAnalytics.anonymity isn't set to Pseudonymous. export interface IPseudonymousEvent extends IEvent {} -// If an event extends IAnonymousEvent, the event strictly contains *only* anonymous data which -// may be sent without explicit user consent. +// If an event extends IAnonymousEvent, the event strictly contains *only* anonymous data; +// i.e. no identifiers that can be associated with the user. export interface IAnonymousEvent extends IEvent {} export interface IRoomEvent extends IPseudonymousEvent { @@ -83,6 +84,23 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p } export class PosthogAnalytics { + /* Wrapper for Posthog analytics. + * + * 3 modes of anonymity are supported, governed by this.anonymity + * - Anonymity.Disabled means *no data* is passed to posthog + * - Anonymity.Anonymous means all identifers will be redacted before being passed to posthog + * - Anonymity.Pseudonymous means all identifiers will be hashed via SHA-256 before being passed + * to Posthog + * + * To update anonymity, call updateAnonymityFromSettings() or you can set it directly via setAnonymity(). + * + * To pass an event to Posthog: + * + * 1. Declare a type for the event, extending IAnonymousEvent, IPseudonymousEvent or IRoomEvent. + * 2. Call the appropriate track*() method. Pseudonymous events will be dropped when anonymity is + * Anonymous or Disabled; Anonymous events will be dropped when anonymity is Disabled. + */ + private anonymity = Anonymity.Anonymous; private posthog?: PostHog = null; // set true during the constructor if posthog config is present, otherwise false @@ -156,9 +174,9 @@ export class PosthogAnalytics { // "Send anonymous usage data which helps us improve Element. This will use a cookie." const analyticsOptIn = SettingsStore.getValue("analyticsOptIn"); - // "Send pseudonymous usage data which helps us improve Element. This will use a cookie." + // (proposed wording) "Send pseudonymous usage data which helps us improve Element. This will use a cookie." // - // Currently, this is only a labs flag, for testing purposes. + // TODO: Currently, this is only a labs flag, for testing purposes. const pseudonumousOptIn = SettingsStore.getValue("feature_pseudonymousAnalyticsOptIn"); let anonymity; @@ -173,23 +191,46 @@ export class PosthogAnalytics { return anonymity; } - public async identifyUser(userId: string) { - if (this.anonymity == Anonymity.Pseudonymous) { - this.posthog.identify(await hashHex(userId)); - } - } - private registerSuperProperties(properties) { if (this.enabled) { this.posthog.register(properties); } } + private static async getPlatformProperties() { + const platform = PlatformPeg.get(); + let appVersion; + try { + appVersion = await platform.getAppVersion(); + } catch (e) { + // this happens if no version is set i.e. in dev + appVersion = "unknown"; + } + + return { + appVersion, + appPlatform: platform.getHumanReadableName(), + }; + } + + private async capture(eventName: string, properties: posthog.Properties) { + if (!this.enabled) { + return; + } + const { origin, hash, pathname } = window.location; + properties['$redacted_current_url'] = await getRedactedCurrentLocation( + origin, hash, pathname, this.anonymity); + this.posthog.capture(eventName, properties); + } + public isEnabled() { return this.enabled; } public setAnonymity(anonymity: Anonymity) { + // Update this.anonymity. + // This is public for testing purposes, typically you want to call updateAnonymityFromSettings + // to ensure this value is in step with the user's settings. if (this.enabled && (anonymity == Anonymity.Disabled || anonymity == Anonymity.Anonymous)) { // when transitioning to Disabled or Anonymous ensure we clear out any prior state // set in posthog e.g. distinct ID @@ -200,6 +241,12 @@ export class PosthogAnalytics { this.anonymity = anonymity; } + public async identifyUser(userId: string) { + if (this.anonymity == Anonymity.Pseudonymous) { + this.posthog.identify(await hashHex(userId)); + } + } + public getAnonymity() { return this.anonymity; } @@ -211,16 +258,6 @@ export class PosthogAnalytics { this.setAnonymity(Anonymity.Anonymous); } - private async capture(eventName: string, properties: posthog.Properties) { - if (!this.enabled) { - return; - } - const { origin, hash, pathname } = window.location; - properties['$redacted_current_url'] = await getRedactedCurrentLocation( - origin, hash, pathname, this.anonymity); - this.posthog.capture(eventName, properties); - } - public async trackPseudonymousEvent( eventName: E["eventName"], properties: E["properties"], @@ -256,27 +293,18 @@ export class PosthogAnalytics { } public async updatePlatformSuperProperties() { + // Update super properties in posthog with our platform (app version, platform). + // These properties will be subsequently passed in every event. + // + // This only needs to be done once per page lifetime. Note that getPlatformProperties + // is async and can involve a network request if we are running in a browser. this.platformSuperProperties = await PosthogAnalytics.getPlatformProperties(); this.registerSuperProperties(this.platformSuperProperties); } - private static async getPlatformProperties() { - const platform = PlatformPeg.get(); - let appVersion; - try { - appVersion = await platform.getAppVersion(); - } catch (e) { - // this happens if no version is set i.e. in dev - appVersion = "unknown"; - } - - return { - appVersion, - appPlatform: platform.getHumanReadableName(), - }; - } - public async updateAnonymityFromSettings(userId?: string) { + // Update this.anonymity based on the user's analytics opt-in settings + // Identify the user (via hashed user ID) to posthog if anonymity is pseudonmyous this.setAnonymity(PosthogAnalytics.getAnonymityFromSettings()); if (userId && this.getAnonymity() == Anonymity.Pseudonymous) { await this.identifyUser(userId); diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index f726fe0b13..a0cfec2406 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -172,7 +172,7 @@ bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`); expect(location).toBe("https://foo.bar/#///"); }); - it("Should currently handle an empty hash", async () => { + it("Should handle an empty hash", async () => { const location = await getRedactedCurrentLocation( "https://foo.bar", "", "/", Anonymity.Anonymous); expect(location).toBe("https://foo.bar/"); From 4048cb3c37132596ae52a3b5c2f336201213a3ac Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 10:20:16 +0100 Subject: [PATCH 052/115] Default to Anonymous tracking when no OptIn setting is present --- src/PosthogAnalytics.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 6329598685..bce6548cb3 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -172,17 +172,19 @@ export class PosthogAnalytics { // determine the current anonymity level based on curernt user settings // "Send anonymous usage data which helps us improve Element. This will use a cookie." - const analyticsOptIn = SettingsStore.getValue("analyticsOptIn"); + const analyticsOptIn = SettingsStore.getValue("analyticsOptIn", null, true); // (proposed wording) "Send pseudonymous usage data which helps us improve Element. This will use a cookie." // // TODO: Currently, this is only a labs flag, for testing purposes. - const pseudonumousOptIn = SettingsStore.getValue("feature_pseudonymousAnalyticsOptIn"); + const pseudonumousOptIn = SettingsStore.getValue("feature_pseudonymousAnalyticsOptIn", null, true); let anonymity; if (pseudonumousOptIn) { anonymity = Anonymity.Pseudonymous; - } else if (analyticsOptIn) { + } else if (analyticsOptIn || analyticsOptIn === null) { + // If no analyticsOptIn has been set (i.e. before the user has logged in, or if they haven't answered the + // opt-in question, assume Anonymous) anonymity = Anonymity.Anonymous; } else { anonymity = Anonymity.Disabled; From c206127f68f8ab72a42fa55d38aeb88604fd84b8 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 10:45:03 +0100 Subject: [PATCH 053/115] Track screen name when tracking page view --- src/PosthogAnalytics.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index bce6548cb3..e28972060a 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -36,6 +36,7 @@ interface IPageView extends IAnonymousEvent { eventName: "$pageview", properties: { durationMs?: number + screen?: string } } @@ -289,8 +290,17 @@ export class PosthogAnalytics { } public async trackPageView(durationMs: number) { + const hash = window.location.hash; + + let screen = null; + const split = hash.split("/"); + if (split.length >= 2) { + screen = split[1]; + } + await this.trackAnonymousEvent("$pageview", { durationMs, + screen, }); } From c3e715c1ca525beff68c8fa03ecbe64c4d8df6f3 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 10:45:32 +0100 Subject: [PATCH 054/115] i18n --- src/i18n/strings/en_EN.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2790e17eed..403374edb4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -821,6 +821,7 @@ "Show message previews for reactions in DMs": "Show message previews for reactions in DMs", "Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms", "Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices", + "Send pseudonymous analytics data": "Send pseudonymous analytics data", "Enable advanced debugging for the room list": "Enable advanced debugging for the room list", "Show info about bridges in room settings": "Show info about bridges in room settings", "Font size": "Font size", From 8ef18d0f9a97067676741349710853faac16fc07 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 11:01:59 +0100 Subject: [PATCH 055/115] Add module level comment about anonymity behaviour --- src/PosthogAnalytics.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index e28972060a..0435a0f22c 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -3,6 +3,21 @@ import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; import SettingsStore from './settings/SettingsStore'; +/* Posthog analytics tracking. + * + * Anonymity behaviour is as follows: + * + * - If Posthog isn't configured in `config.json`, events are not sent. + * - If [Do Not Track](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack) is + * enabled, events are not sent (this detection is built into posthog and turned on via the + * `respect_dnt` flag being passed to `posthog.init`). + * - If the `feature_pseudonymousAnalyticsOptIn` labs flag is `true`, track pseudonomously, i.e. + * hash all matrix identifiers in tracking events (user IDs, room IDs etc) using SHA-256. + * - Otherwise, if the existing `analyticsOptIn` flag is `true`, or not present (i.e. prior to + * logging in), track anonymously, i.e. redact all matrix identifiers in tracking events. + * - If both flags are false, events are not sent. +*/ + interface IEvent { // The event name that will be used by PostHog. // TODO: standard format (camel case? snake? UpperCase?) @@ -86,7 +101,6 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p export class PosthogAnalytics { /* Wrapper for Posthog analytics. - * * 3 modes of anonymity are supported, governed by this.anonymity * - Anonymity.Disabled means *no data* is passed to posthog * - Anonymity.Anonymous means all identifers will be redacted before being passed to posthog From c697079eb42727f84cfb6b9f79d73931710d10b9 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 11:22:40 +0100 Subject: [PATCH 056/115] Fix import --- test/PosthogAnalytics-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index a0cfec2406..095e216262 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -1,7 +1,7 @@ import { Anonymity, getRedactedCurrentLocation, IAnonymousEvent, IPseudonymousEvent, IRoomEvent, PosthogAnalytics } from '../src/PosthogAnalytics'; import SdkConfig from '../src/SdkConfig'; -const crypto = require('crypto'); +import crypto = require('crypto'); class FakePosthog { public capture; From 7c62386915b2e027d260c77d700610bea16791ef Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 11:28:19 +0100 Subject: [PATCH 057/115] lint --- src/PosthogAnalytics.ts | 14 +++++++------- test/PosthogAnalytics-test.ts | 18 +++++++++--------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 0435a0f22c..ee45f57e70 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -24,7 +24,7 @@ interface IEvent { eventName: string; // The properties of the event that will be stored in PostHog. - properties: {} + properties: {}; } export enum Anonymity { @@ -44,19 +44,19 @@ export interface IPseudonymousEvent extends IEvent {} export interface IAnonymousEvent extends IEvent {} export interface IRoomEvent extends IPseudonymousEvent { - hashedRoomId: string + hashedRoomId: string; } interface IPageView extends IAnonymousEvent { - eventName: "$pageview", + eventName: "$pageview"; properties: { - durationMs?: number - screen?: string - } + durationMs?: number; + screen?: string; + }; } export interface IWelcomeScreenLoad extends IAnonymousEvent { - eventName: "welcome_screen_load", + eventName: "welcome_screen_load"; } const hashHex = async (input: string): Promise => { diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index 095e216262..920f449bab 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -20,24 +20,24 @@ class FakePosthog { } export interface ITestEvent extends IAnonymousEvent { - key: "jest_test_event", + key: "jest_test_event"; properties: { - foo: string - } + foo: string; + }; } export interface ITestPseudonymousEvent extends IPseudonymousEvent { - key: "jest_test_pseudo_event", + key: "jest_test_pseudo_event"; properties: { - foo: string - } + foo: string; + }; } export interface ITestRoomEvent extends IRoomEvent { - key: "jest_test_room_event", + key: "jest_test_room_event"; properties: { - foo: string - } + foo: string; + }; } describe("PosthogAnalytics", () => { From 7cf28de9c90a53708821ce4a7cb82752f4401502 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 11:32:17 +0100 Subject: [PATCH 058/115] take 2 at fixing import --- test/PosthogAnalytics-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index 920f449bab..57eb8ed72b 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -1,7 +1,7 @@ import { Anonymity, getRedactedCurrentLocation, IAnonymousEvent, IPseudonymousEvent, IRoomEvent, PosthogAnalytics } from '../src/PosthogAnalytics'; import SdkConfig from '../src/SdkConfig'; -import crypto = require('crypto'); +import crypto from 'crypto'; class FakePosthog { public capture; From d96e7e3375d64931af8eb9bebf49664a3fde0df6 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 12:08:55 +0100 Subject: [PATCH 059/115] Add transitive dev dependencies of posthog This is needed during tsc lint as posthog imports types from these libraries into its type definitions --- package.json | 4 +++- yarn.lock | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c0062db46c..084f413605 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "what-input": "^5.2.10", "zxcvbn": "^4.4.2" }, - "devDependencies": { + "devDependencies": { "@babel/cli": "^7.12.10", "@babel/core": "^7.12.10", "@babel/eslint-parser": "^7.12.10", @@ -125,6 +125,7 @@ "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", "@peculiar/webcrypto": "^1.1.4", "@sinonjs/fake-timers": "^7.0.2", + "@sentry/types": "^6.2.2", "@types/classnames": "^2.2.11", "@types/commonmark": "^0.27.4", "@types/counterpart": "^0.18.1", @@ -167,6 +168,7 @@ "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", "react-test-renderer": "^17.0.2", "rimraf": "^3.0.2", + "rrweb": "^0.9.9", "stylelint": "^13.9.0", "stylelint-config-standard": "^20.0.0", "stylelint-scss": "^3.18.0", diff --git a/yarn.lock b/yarn.lock index c4d1456612..78f4838a09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1352,6 +1352,11 @@ tslib "^2.2.0" webcrypto-core "^1.2.0" +"@sentry/types@^6.2.2": + version "6.10.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.10.0.tgz#6b1f44e5ed4dbc2710bead24d1b32fb08daf04e1" + integrity sha512-M7s0JFgG7/6/yNVYoPUbxzaXDhnzyIQYRRJJKRaTD77YO4MHvi4Ke8alBWqD5fer0cPIfcSkBqa9BLdqRqcMWw== + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -1448,6 +1453,11 @@ resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.1.tgz#b1b784d9e54d9879f0a8cb12f2caedab65430fe8" integrity sha512-PRuFlBBkvdDOtxlIASzTmkEFar+S66Ek48NVVTWMUjtJAdn5vyMSN8y6IZIoIymGpR36q2nZbIYazBWyFxL+IQ== +"@types/css-font-loading-module@0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.4.tgz#94a835e27d1af444c65cba88523533c174463d64" + integrity sha512-ENdXf7MW4m9HeDojB2Ukbi7lYMIuQNBHVf98dbzaiG4EEJREBd6oleVAjrLRCrp7dm6CK1mmdmU9tcgF61acbw== + "@types/css-font-loading-module@^0.0.6": version "0.0.6" resolved "https://registry.yarnpkg.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.6.tgz#1ac3417ed31eeb953134d29b56bca921644b87c0" @@ -1790,6 +1800,11 @@ object.fromentries "^2.0.0" prop-types "^15.7.0" +"@xstate/fsm@^1.4.0": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@xstate/fsm/-/fsm-1.6.1.tgz#c92972b835540c4e3c5e14277f40dbcbdaee9571" + integrity sha512-xYKDNuPR36/fUK+jmhM+oauBmbdUAfuJKnDjg3/7NbN+Pj03TX7e94LXnzkwGgAR+U/HWoMqM5UPTuGIYfIx9g== + abab@^2.0.3, abab@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" @@ -3601,7 +3616,7 @@ fbjs@^0.8.4: setimmediate "^1.0.5" ua-parser-js "^0.7.18" -fflate@^0.4.1: +fflate@^0.4.1, fflate@^0.4.4: version "0.4.8" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== @@ -5639,6 +5654,11 @@ minimist@>=1.2.2, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +mitt@^1.1.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-1.2.0.tgz#cb24e6569c806e31bd4e3995787fe38a04fdf90d" + integrity sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw== + mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" @@ -6841,6 +6861,22 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rrweb-snapshot@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.1.7.tgz#92a3b47b1112a1b566c2fae2edb02fa48a6f6653" + integrity sha512-+f2kCCvIQ1hbEeCWnV7mPVPDEdWEExqwcYqMd/r1nfK52QE7qU52jefUOyTe85Vy67rZGqWnfK/B25e/OTSgYg== + +rrweb@^0.9.9: + version "0.9.14" + resolved "https://registry.yarnpkg.com/rrweb/-/rrweb-0.9.14.tgz#09bec604fc44c74801e4fe910606e5a6cde008ec" + integrity sha512-nm2rrVNoyWFPrbGQmcvTTlA7XjbbgPIgO7qsW0Zyr5iOURIFJDGPHFmOVLRyLpWiriVtEoXh6a+x+D1sj+qwWg== + dependencies: + "@types/css-font-loading-module" "0.0.4" + "@xstate/fsm" "^1.4.0" + fflate "^0.4.4" + mitt "^1.1.3" + rrweb-snapshot "^1.0.3" + rst-selector-parser@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" From 55e8173ee9011e6dff733090f6671639244be692 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 12:12:10 +0100 Subject: [PATCH 060/115] remove whitespace --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 084f413605..e5ecbb31a9 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "what-input": "^5.2.10", "zxcvbn": "^4.4.2" }, - "devDependencies": { + "devDependencies": { "@babel/cli": "^7.12.10", "@babel/core": "^7.12.10", "@babel/eslint-parser": "^7.12.10", From 3ff7de3c967a7937830fcddf5e6785b1e0c5ff5a Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 13:43:06 +0100 Subject: [PATCH 061/115] Mock SHA-256 to avoid problems loading crypto on Node 14 --- test/PosthogAnalytics-test.ts | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index 57eb8ed72b..b7fae3c196 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -1,7 +1,6 @@ import { Anonymity, getRedactedCurrentLocation, IAnonymousEvent, IPseudonymousEvent, IRoomEvent, PosthogAnalytics } from '../src/PosthogAnalytics'; import SdkConfig from '../src/SdkConfig'; -import crypto from 'crypto'; class FakePosthog { public capture; @@ -42,12 +41,37 @@ export interface ITestRoomEvent extends IRoomEvent { describe("PosthogAnalytics", () => { let fakePosthog: FakePosthog; + const shaHashes = { + "42": "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049", + "some": "a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b", + "pii": "bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4", + "foo": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", + }; beforeEach(() => { fakePosthog = new FakePosthog(); window.crypto = { - subtle: crypto.webcrypto.subtle, + subtle: { + digest: async (_, encodedMessage) => { + const message = new TextDecoder().decode(encodedMessage); + const hexHash = shaHashes[message]; + const bytes = []; + for (let c = 0; c < hexHash.length; c += 2) { + bytes.push(parseInt(hexHash.substr(c, 2), 16)); + } + return bytes; + }, + /*console.log(message); + const digest = sha256(new WordArray(message)); + const digestBuf = new ArrayBuffer(digest.words.length * 4); + console.log(digest); + const view = new Uint32Array(digestBuf); + for (let i = 0; i < digest.words.length; i++) { + view[i] = digest.words[i]; + } + return digestBuf*/ + }, }; }); @@ -135,7 +159,7 @@ describe("PosthogAnalytics", () => { await analytics.trackAnonymousEvent("jest_test_event", { foo: "bar", }); - await analytics.trackRoomEvent("room id", "jest_test_room_event", { + await analytics.trackRoomEvent("room id", "foo", { foo: "bar", }); await analytics.trackPageView(200); From df7ebb2e7ce5894afd349f3a1f8cf930f80f5f90 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:00:37 +0100 Subject: [PATCH 062/115] Remove commented out block --- test/PosthogAnalytics-test.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index b7fae3c196..9b8e703c8e 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -62,15 +62,6 @@ describe("PosthogAnalytics", () => { } return bytes; }, - /*console.log(message); - const digest = sha256(new WordArray(message)); - const digestBuf = new ArrayBuffer(digest.words.length * 4); - console.log(digest); - const view = new Uint32Array(digestBuf); - for (let i = 0; i < digest.words.length; i++) { - view[i] = digest.words[i]; - } - return digestBuf*/ }, }; }); From 868d92781d402a46a5ec758c8ad10a77ef11f27d Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:01:21 +0100 Subject: [PATCH 063/115] Add copyright header --- src/PosthogAnalytics.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index ee45f57e70..9628ed1e4e 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -1,3 +1,19 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import posthog, { PostHog } from 'posthog-js'; import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; From d5bef53f8bc9f7c523afdc78c3c0c3500e268a30 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:03:17 +0100 Subject: [PATCH 064/115] Use snake case for feature name --- src/PosthogAnalytics.ts | 4 ++-- src/settings/Settings.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 9628ed1e4e..15bd10ad67 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -27,7 +27,7 @@ import SettingsStore from './settings/SettingsStore'; * - If [Do Not Track](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack) is * enabled, events are not sent (this detection is built into posthog and turned on via the * `respect_dnt` flag being passed to `posthog.init`). - * - If the `feature_pseudonymousAnalyticsOptIn` labs flag is `true`, track pseudonomously, i.e. + * - If the `feature_pseudonymous_analytics_opt_in` labs flag is `true`, track pseudonomously, i.e. * hash all matrix identifiers in tracking events (user IDs, room IDs etc) using SHA-256. * - Otherwise, if the existing `analyticsOptIn` flag is `true`, or not present (i.e. prior to * logging in), track anonymously, i.e. redact all matrix identifiers in tracking events. @@ -208,7 +208,7 @@ export class PosthogAnalytics { // (proposed wording) "Send pseudonymous usage data which helps us improve Element. This will use a cookie." // // TODO: Currently, this is only a labs flag, for testing purposes. - const pseudonumousOptIn = SettingsStore.getValue("feature_pseudonymousAnalyticsOptIn", null, true); + const pseudonumousOptIn = SettingsStore.getValue("feature_pseudonymous_analytics_opt_in", null, true); let anonymity; if (pseudonumousOptIn) { diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 810d8bb323..c287a3fd9d 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -299,7 +299,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_FEATURE, default: false, }, - "feature_pseudonymousAnalyticsOptIn": { + "feature_pseudonymous_analytics_opt_in": { isFeature: true, supportedLevels: LEVELS_FEATURE, displayName: _td('Send pseudonymous analytics data'), From 91e65534fa270fc22f62e30f77585ac1f67689c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 28 Jul 2021 15:04:33 +0200 Subject: [PATCH 065/115] await setState to avoid races where we would try to play media without an HTMLVideoElement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/VideoFeed.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index fef3aa0691..ad5b6f42fd 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -140,16 +140,16 @@ export default class VideoFeed extends React.Component { // seem to be necessary - Šimon } - private onNewStream = () => { - this.setState({ + private onNewStream = async () => { + await this.setState({ audioMuted: this.props.feed.isAudioMuted(), videoMuted: this.props.feed.isVideoMuted(), }); this.playMedia(); }; - private onMuteStateChanged = () => { - this.setState({ + private onMuteStateChanged = async () => { + await this.setState({ audioMuted: this.props.feed.isAudioMuted(), videoMuted: this.props.feed.isVideoMuted(), }); From 7c4e3efbff953c100efcd30e813c2447f9527775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 28 Jul 2021 15:11:31 +0200 Subject: [PATCH 066/115] Extend PureComponent to avoid unnecessary renders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/VideoFeed.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index ad5b6f42fd..41c6b5185c 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -47,7 +47,7 @@ interface IState { } @replaceableComponent("views.voip.VideoFeed") -export default class VideoFeed extends React.Component { +export default class VideoFeed extends React.PureComponent { private element: HTMLVideoElement; constructor(props: IProps) { From a09e046c18d62dd3f14f18b6bf991f646bcad9d8 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:11:55 +0100 Subject: [PATCH 067/115] Update test/PosthogAnalytics-test.ts Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- test/PosthogAnalytics-test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index 9b8e703c8e..d80f2946c3 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -1,5 +1,11 @@ -import { Anonymity, getRedactedCurrentLocation, IAnonymousEvent, IPseudonymousEvent, IRoomEvent, - PosthogAnalytics } from '../src/PosthogAnalytics'; +import { + Anonymity, + getRedactedCurrentLocation, + IAnonymousEvent, + IPseudonymousEvent, + IRoomEvent, + PosthogAnalytics, +} from '../src/PosthogAnalytics'; import SdkConfig from '../src/SdkConfig'; class FakePosthog { From ecbc536a3eb67bee130793a396fa2693ceb02f74 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:15:27 +0100 Subject: [PATCH 068/115] Add copyright header --- test/PosthogAnalytics-test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts index d80f2946c3..6cb1743051 100644 --- a/test/PosthogAnalytics-test.ts +++ b/test/PosthogAnalytics-test.ts @@ -1,3 +1,19 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import { Anonymity, getRedactedCurrentLocation, @@ -6,6 +22,7 @@ import { IRoomEvent, PosthogAnalytics, } from '../src/PosthogAnalytics'; + import SdkConfig from '../src/SdkConfig'; class FakePosthog { From da3bf5a097b437b912d5d1ad78e00e5f0aceb7c2 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:14:15 +0100 Subject: [PATCH 069/115] rename knownScreens -> whitelistedScreens --- src/PosthogAnalytics.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 15bd10ad67..66f17a4937 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -83,7 +83,7 @@ const hashHex = async (input: string): Promise => { return [...new Uint8Array(digestBuf)].map((b: number) => b.toString(16).padStart(2, "0")).join(""); }; -const knownScreens = new Set([ +const whitelistedScreens = new Set([ "register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory", "start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group", ]); @@ -102,7 +102,7 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p } else { let [_, screen, ...parts] = hash.split("/"); - if (!knownScreens.has(screen)) { + if (!whitelistedScreens.has(screen)) { screen = ""; } From 9420b81eebdfc2b4edb3d6dcf0f179c4d29e358c Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:22:32 +0100 Subject: [PATCH 070/115] Rename mysterious _ to beforeFirstSlash --- src/PosthogAnalytics.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 66f17a4937..7331b2edd1 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -100,7 +100,7 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p if (hash == "") { hashStr = ""; } else { - let [_, screen, ...parts] = hash.split("/"); + let [beforeFirstSlash, screen, ...parts] = hash.split("/"); if (!whitelistedScreens.has(screen)) { screen = ""; @@ -110,7 +110,7 @@ export async function getRedactedCurrentLocation(origin: string, hash: string, p parts[i] = anonymity === Anonymity.Anonymous ? `` : await hashHex(parts[i]); } - hashStr = `${_}/${screen}/${parts.join("/")}`; + hashStr = `${beforeFirstSlash}/${screen}/${parts.join("/")}`; } return origin + pathname + hashStr; } From 60bc283455f22e3bbf640b538e9590a28dea4ef7 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:22:57 +0100 Subject: [PATCH 071/115] Add return type to getRedactedCurrentLocation --- src/PosthogAnalytics.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 7331b2edd1..9ba33e37c1 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -88,7 +88,12 @@ const whitelistedScreens = new Set([ "start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group", ]); -export async function getRedactedCurrentLocation(origin: string, hash: string, pathname: string, anonymity: Anonymity) { +export async function getRedactedCurrentLocation( + origin: string, + hash: string, + pathname: string, + anonymity: Anonymity, +): Promise { // Redact PII from the current location. // If anonymous is true, redact entirely, if false, substitute it with a hash. // For known screens, assumes a URL structure of //might/be/pii From a687bab52f6e5bcd66b9883a9a519b1fff54714e Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:38:41 +0100 Subject: [PATCH 072/115] Use readonly shorthand for posthog param --- src/PosthogAnalytics.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 9ba33e37c1..d10ea01f4e 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -138,7 +138,6 @@ export class PosthogAnalytics { */ private anonymity = Anonymity.Anonymous; - private posthog?: PostHog = null; // set true during the constructor if posthog config is present, otherwise false private enabled = false; private static _instance = null; @@ -151,8 +150,7 @@ export class PosthogAnalytics { return this._instance; } - constructor(posthog: PostHog) { - this.posthog = posthog; + constructor(private readonly posthog: PostHog) { const posthogConfig = SdkConfig.get()["posthog"]; if (posthogConfig) { this.posthog.init(posthogConfig.projectApiKey, { From df6d772d8d1df34988884d2af3acb6664657f8e0 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:40:32 +0100 Subject: [PATCH 073/115] Pin posthog version We'd like to manually review each posthog change to avoid unanticipated tracking leakages; each upgrade should include reviewing the data coming in on events --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5ecbb31a9..bd989ac9bc 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "pako": "^2.0.3", "parse5": "^6.0.1", "png-chunks-extract": "^1.0.0", - "posthog-js": "^1.12.1", + "posthog-js": "1.12.1", "prop-types": "^15.7.2", "qrcode": "^1.4.4", "re-resizable": "^6.9.0", From 279871ce01f6e9c01dc5d5f691bcb01b0afb9c90 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:44:16 +0100 Subject: [PATCH 074/115] Add types --- src/PosthogAnalytics.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index d10ea01f4e..345f778c89 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -120,6 +120,11 @@ export async function getRedactedCurrentLocation( return origin + pathname + hashStr; } +interface PlatformProperties { + appVersion: string, + appPlatform: string +} + export class PosthogAnalytics { /* Wrapper for Posthog analytics. * 3 modes of anonymity are supported, governed by this.anonymity @@ -227,13 +232,13 @@ export class PosthogAnalytics { return anonymity; } - private registerSuperProperties(properties) { + private registerSuperProperties(properties: posthog.Properties) { if (this.enabled) { this.posthog.register(properties); } } - private static async getPlatformProperties() { + private static async getPlatformProperties(): Promise { const platform = PlatformPeg.get(); let appVersion; try { From ce80e5a4639cd1d698e58e229c609a50e0870743 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 14:44:28 +0100 Subject: [PATCH 075/115] Remove superfluous unused argument --- src/PosthogAnalytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 345f778c89..7aae756894 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -178,7 +178,7 @@ export class PosthogAnalytics { } } - private sanitizeProperties(properties: posthog.Properties, _: string): posthog.Properties { + private sanitizeProperties(properties: posthog.Properties): posthog.Properties { // Callback from posthog to sanitize properties before sending them to the server. // // Here we sanitize posthog's built in properties which leak PII e.g. url reporting. From 537ce40f429c0284c7ba837f6e7912238242b9cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 28 Jul 2021 16:32:55 +0200 Subject: [PATCH 076/115] Add a TODO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/VideoFeed.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 41c6b5185c..9975f70d62 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -46,6 +46,7 @@ interface IState { videoMuted: boolean; } +// TODO: We shouldn't be calling playMedia() all the time @replaceableComponent("views.voip.VideoFeed") export default class VideoFeed extends React.PureComponent { private element: HTMLVideoElement; From 13ef819ba6a36e3c3d39f4de373a32ec284495d2 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 16:42:56 +0100 Subject: [PATCH 077/115] isEnabled returns a boolean --- src/PosthogAnalytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 7aae756894..7e7703a9aa 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -264,7 +264,7 @@ export class PosthogAnalytics { this.posthog.capture(eventName, properties); } - public isEnabled() { + public isEnabled(): boolean { return this.enabled; } From b1bd5f57a4deb3674e59179e0e72962dc99d1edd Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 16:43:13 +0100 Subject: [PATCH 078/115] Document IEvent.properties, fix IWelcomeScreenLoad IEvent.properties is a placeholder that needs to be overriden by extenders for type validation to take place. IWelcomeScreenLoad should have had properties declared for it. Because it didn't, a faulty call using it was possible. --- src/PosthogAnalytics.ts | 11 ++++++----- src/components/views/auth/Welcome.tsx | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 7e7703a9aa..63bfbda72e 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -32,15 +32,15 @@ import SettingsStore from './settings/SettingsStore'; * - Otherwise, if the existing `analyticsOptIn` flag is `true`, or not present (i.e. prior to * logging in), track anonymously, i.e. redact all matrix identifiers in tracking events. * - If both flags are false, events are not sent. -*/ + */ interface IEvent { - // The event name that will be used by PostHog. - // TODO: standard format (camel case? snake? UpperCase?) + // The event name that will be used by PostHog. Event names should use snake_case. eventName: string; - // The properties of the event that will be stored in PostHog. - properties: {}; + // The properties of the event that will be stored in PostHog. This is just a placeholder, + // extending interfaces must override this with a concrete definition to do type validation. + properties: {} } export enum Anonymity { @@ -73,6 +73,7 @@ interface IPageView extends IAnonymousEvent { export interface IWelcomeScreenLoad extends IAnonymousEvent { eventName: "welcome_screen_load"; + properties: Record; } const hashHex = async (input: string): Promise => { diff --git a/src/components/views/auth/Welcome.tsx b/src/components/views/auth/Welcome.tsx index 4ba603eaf4..75bbe15411 100644 --- a/src/components/views/auth/Welcome.tsx +++ b/src/components/views/auth/Welcome.tsx @@ -76,6 +76,6 @@ export default class Welcome extends React.PureComponent { } componentDidMount() { - getAnalytics().trackAnonymousEvent("welcome_screen_load", { foo: "bar" }); + getAnalytics().trackAnonymousEvent("welcome_screen_load", {}); } } From e5d36e9a81a387022094db844c9a66835b285a2d Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 17:02:26 +0100 Subject: [PATCH 079/115] Use arrow function instead of bind --- src/PosthogAnalytics.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 63bfbda72e..8f338f6012 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -170,7 +170,7 @@ export class PosthogAnalytics { // // To raise this manually, just call .capture("$pageview") or posthog.capture_pageview. capture_pageview: false, - sanitize_properties: this.sanitizeProperties.bind(this), + sanitize_properties: this.sanitizeProperties, respect_dnt: true, }); this.enabled = true; @@ -179,7 +179,7 @@ export class PosthogAnalytics { } } - private sanitizeProperties(properties: posthog.Properties): posthog.Properties { + private sanitizeProperties = (properties: posthog.Properties): posthog.Properties => { // Callback from posthog to sanitize properties before sending them to the server. // // Here we sanitize posthog's built in properties which leak PII e.g. url reporting. @@ -206,7 +206,7 @@ export class PosthogAnalytics { } return properties; - } + }; private static getAnonymityFromSettings(): Anonymity { // determine the current anonymity level based on curernt user settings From 0a951501b2f90c62419fcbd43af6f36616f59f74 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 17:04:18 +0100 Subject: [PATCH 080/115] lint --- src/PosthogAnalytics.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 8f338f6012..6d15ca79ce 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -40,7 +40,7 @@ interface IEvent { // The properties of the event that will be stored in PostHog. This is just a placeholder, // extending interfaces must override this with a concrete definition to do type validation. - properties: {} + properties: {}; } export enum Anonymity { @@ -122,8 +122,8 @@ export async function getRedactedCurrentLocation( } interface PlatformProperties { - appVersion: string, - appPlatform: string + appVersion: string; + appPlatform: string; } export class PosthogAnalytics { From e4722ee4578dc7d06999c68b7e321731c598b1d8 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 17:14:36 +0100 Subject: [PATCH 081/115] Override posthog type definitions to point to a locally fixed type definition file Posthog's type definitions refer to types in transitive dependencies we don't want to include. Clone posthog.d.ts locally, remove the offending types from it, and provide an overriding mapping in tsconfig. If this proves annoying to maintain, posthog.d.ts could just be an empty file. --- package.json | 2 - src/@types/posthog.d.ts | 739 ++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 9 +- yarn.lock | 40 +-- 4 files changed, 748 insertions(+), 42 deletions(-) create mode 100644 src/@types/posthog.d.ts diff --git a/package.json b/package.json index bd989ac9bc..6adb6ce004 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,6 @@ "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", "@peculiar/webcrypto": "^1.1.4", "@sinonjs/fake-timers": "^7.0.2", - "@sentry/types": "^6.2.2", "@types/classnames": "^2.2.11", "@types/commonmark": "^0.27.4", "@types/counterpart": "^0.18.1", @@ -168,7 +167,6 @@ "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", "react-test-renderer": "^17.0.2", "rimraf": "^3.0.2", - "rrweb": "^0.9.9", "stylelint": "^13.9.0", "stylelint-config-standard": "^20.0.0", "stylelint-scss": "^3.18.0", diff --git a/src/@types/posthog.d.ts b/src/@types/posthog.d.ts new file mode 100644 index 0000000000..1108e2c6df --- /dev/null +++ b/src/@types/posthog.d.ts @@ -0,0 +1,739 @@ +// Type definitions for exported methods + +declare class posthog { + /** + * This function initializes a new instance of the PostHog capturing object. + * All new instances are added to the main posthog object as sub properties (such as + * posthog.library_name) and also returned by this function. To define a + * second instance on the page, you would call: + * + * posthog.init('new token', { your: 'config' }, 'library_name'); + * + * and use it like so: + * + * posthog.library_name.capture(...); + * + * @param {String} token Your PostHog API token + * @param {Object} [config] A dictionary of config options to override. See a list of default config options. + * @param {String} [name] The name for the new posthog instance that you want created + */ + static init(token: string, config?: posthog.Config, name?: string): posthog + + /** + * Clears super properties and generates a new random distinct_id for this instance. + * Useful for clearing data when a user logs out. + */ + static reset(reset_device_id?: boolean): void + + /** + * Capture an event. This is the most important and + * frequently used PostHog function. + * + * ### Usage: + * + * // capture an event named 'Registered' + * posthog.capture('Registered', {'Gender': 'Male', 'Age': 21}); + * + * // capture an event using navigator.sendBeacon + * posthog.capture('Left page', {'duration_seconds': 35}, {transport: 'sendBeacon'}); + * + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Object} [options] Optional configuration for this capture request. + * @param {String} [options.transport] Transport method for network request ('XHR' or 'sendBeacon'). + */ + static capture( + event_name: string, + properties?: posthog.Properties, + options?: { transport: 'XHR' | 'sendBeacon' } + ): posthog.CaptureResult + + /** + * Capture a page view event, which is currently ignored by the server. + * This function is called by default on page load unless the + * capture_pageview configuration variable is false. + * + * @param {String} [page] The url of the page to record. If you don't include this, it defaults to the current url. + * @api private + */ + static capture_pageview(page?: string): void + + /** + * Register a set of super properties, which are included with all + * events. This will overwrite previous super property values. + * + * ### Usage: + * + * // register 'Gender' as a super property + * posthog.register({'Gender': 'Female'}); + * + * // register several super properties when a user signs up + * posthog.register({ + * 'Email': 'jdoe@example.com', + * 'Account Type': 'Free' + * }); + * + * @param {Object} properties An associative array of properties to store about the user + * @param {Number} [days] How many days since the user's last visit to store the super properties + */ + static register(properties: posthog.Properties, days?: number): void + + /** + * Register a set of super properties only once. This will not + * overwrite previous super property values, unlike register(). + * + * ### Usage: + * + * // register a super property for the first time only + * posthog.register_once({ + * 'First Login Date': new Date().toISOString() + * }); + * + * ### Notes: + * + * If default_value is specified, current super properties + * with that value will be overwritten. + * + * @param {Object} properties An associative array of properties to store about the user + * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None' + * @param {Number} [days] How many days since the users last visit to store the super properties + */ + static register_once(properties: posthog.Properties, default_value?: posthog.Property, days?: number): void + + /** + * Delete a super property stored with the current user. + * + * @param {String} property The name of the super property to remove + */ + static unregister(property: string): void + + /** + * Identify a user with a unique ID instead of a PostHog + * randomly generated distinct_id. If the method is never called, + * then unique visitors will be identified by a UUID generated + * the first time they visit the site. + * + * If user properties are passed, they are also sent to posthog. + * + * ### Usage: + * + * posthog.identify('[user unique id]') + * posthog.identify('[user unique id]', { email: 'john@example.com' }) + * posthog.identify('[user unique id]', {}, { referral_code: '12345' }) + * + * ### Notes: + * + * You can call this function to overwrite a previously set + * unique ID for the current user. PostHog cannot translate + * between IDs at this time, so when you change a user's ID + * they will appear to be a new user. + * + * When used alone, posthog.identify will change the user's + * distinct_id to the unique ID provided. When used in tandem + * with posthog.alias, it will allow you to identify based on + * unique ID and map that back to the original, anonymous + * distinct_id given to the user upon her first arrival to your + * site (thus connecting anonymous pre-signup activity to + * post-signup activity). Though the two work together, do not + * call identify() at the same time as alias(). Calling the two + * at the same time can cause a race condition, so it is best + * practice to call identify on the original, anonymous ID + * right after you've aliased it. + * + * @param {String} [unique_id] A string that uniquely identifies a user. If not provided, the distinct_id currently in the persistent store (cookie or localStorage) will be used. + * @param {Object} [userProperties] Optional: An associative array of properties to store about the user + * @param {Object} [userPropertiesToSetOnce] Optional: An associative array of properties to store about the user. If property is previously set, this does not override that value. + */ + static identify( + unique_id?: string, + userPropertiesToSet?: posthog.Properties, + userPropertiesToSetOnce?: posthog.Properties + ): void + + /** + * Create an alias, which PostHog will use to link two distinct_ids going forward (not retroactively). + * Multiple aliases can map to the same original ID, but not vice-versa. Aliases can also be chained - the + * following is a valid scenario: + * + * posthog.alias('new_id', 'existing_id'); + * ... + * posthog.alias('newer_id', 'new_id'); + * + * If the original ID is not passed in, we will use the current distinct_id - probably the auto-generated GUID. + * + * ### Notes: + * + * The best practice is to call alias() when a unique ID is first created for a user + * (e.g., when a user first registers for an account and provides an email address). + * alias() should never be called more than once for a given user, except to + * chain a newer ID to a previously new ID, as described above. + * + * @param {String} alias A unique identifier that you want to use for this user in the future. + * @param {String} [original] The current identifier being used for this user. + */ + static alias(alias: string, original?: string): posthog.CaptureResult | number + + /** + * Update the configuration of a posthog library instance. + * + * The default config is: + * + * { + * // HTTP method for capturing requests + * api_method: 'POST' + * + * // transport for sending requests ('XHR' or 'sendBeacon') + * // NB: sendBeacon should only be used for scenarios such as + * // page unload where a "best-effort" attempt to send is + * // acceptable; the sendBeacon API does not support callbacks + * // or any way to know the result of the request. PostHog + * // capturing via sendBeacon will not support any event- + * // batching or retry mechanisms. + * api_transport: 'XHR' + * + * // Automatically capture clicks, form submissions and change events + * autocapture: true + * + * // Capture rage clicks (beta) - useful for session recording + * rageclick: false + * + * // super properties cookie expiration (in days) + * cookie_expiration: 365 + * + * // super properties span subdomains + * cross_subdomain_cookie: true + * + * // debug mode + * debug: false + * + * // if this is true, the posthog cookie or localStorage entry + * // will be deleted, and no user persistence will take place + * disable_persistence: false + * + * // if this is true, PostHog will automatically determine + * // City, Region and Country data using the IP address of + * //the client + * ip: true + * + * // opt users out of capturing by this PostHog instance by default + * opt_out_capturing_by_default: false + * + * // opt users out of browser data storage by this PostHog instance by default + * opt_out_persistence_by_default: false + * + * // persistence mechanism used by opt-in/opt-out methods - cookie + * // or localStorage - falls back to cookie if localStorage is unavailable + * opt_out_capturing_persistence_type: 'localStorage' + * + * // customize the name of cookie/localStorage set by opt-in/opt-out methods + * opt_out_capturing_cookie_prefix: null + * + * // type of persistent store for super properties (cookie/ + * // localStorage) if set to 'localStorage', any existing + * // posthog cookie value with the same persistence_name + * // will be transferred to localStorage and deleted + * persistence: 'cookie' + * + * // name for super properties persistent store + * persistence_name: '' + * + * // names of properties/superproperties which should never + * // be sent with capture() calls + * property_blacklist: [] + * + * // if this is true, posthog cookies will be marked as + * // secure, meaning they will only be transmitted over https + * secure_cookie: false + * + * // should we capture a page view on page load + * capture_pageview: true + * + * // if you set upgrade to be true, the library will check for + * // a cookie from our old js library and import super + * // properties from it, then the old cookie is deleted + * // The upgrade config option only works in the initialization, + * // so make sure you set it when you create the library. + * upgrade: false + * + * // extra HTTP request headers to set for each API request, in + * // the format {'Header-Name': value} + * xhr_headers: {} + * + * // protocol for fetching in-app message resources, e.g. + * // 'https://' or 'http://'; defaults to '//' (which defers to the + * // current page's protocol) + * inapp_protocol: '//' + * + * // whether to open in-app message link in new tab/window + * inapp_link_new_window: false + * + * // a set of rrweb config options that PostHog users can configure + * // see https://github.com/rrweb-io/rrweb/blob/master/guide.md + * session_recording: { + * blockClass: 'ph-no-capture', + * blockSelector: null, + * ignoreClass: 'ph-ignore-input', + * maskAllInputs: false, + * maskInputOptions: {}, + * maskInputFn: null, + * slimDOMOptions: {}, + * collectFonts: false + * } + * + * // prevent autocapture from capturing any attribute names on elements + * mask_all_element_attributes: false + * + * // prevent autocapture from capturing textContent on all elements + * mask_all_text: false + * + * // will disable requests to the /decide endpoint (please review documentation for details) + * // autocapture, feature flags, compression and session recording will be disabled when set to `true` + * advanced_disable_decide: false + * + * } + * + * + * @param {Object} config A dictionary of new configuration values to update + */ + static set_config(config: posthog.Config): void + + /** + * returns the current config object for the library. + */ + static get_config(prop_name: T): posthog.Config[T] + + /** + * Returns the value of the super property named property_name. If no such + * property is set, get_property() will return the undefined value. + * + * ### Notes: + * + * get_property() can only be called after the PostHog library has finished loading. + * init() has a loaded function available to handle this automatically. For example: + * + * // grab value for 'user_id' after the posthog library has loaded + * posthog.init('YOUR PROJECT TOKEN', { + * loaded: function(posthog) { + * user_id = posthog.get_property('user_id'); + * } + * }); + * + * @param {String} property_name The name of the super property you want to retrieve + */ + static get_property(property_name: string): posthog.Property | undefined + + /** + * Returns the current distinct id of the user. This is either the id automatically + * generated by the library or the id that has been passed by a call to identify(). + * + * ### Notes: + * + * get_distinct_id() can only be called after the PostHog library has finished loading. + * init() has a loaded function available to handle this automatically. For example: + * + * // set distinct_id after the posthog library has loaded + * posthog.init('YOUR PROJECT TOKEN', { + * loaded: function(posthog) { + * distinct_id = posthog.get_distinct_id(); + * } + * }); + */ + static get_distinct_id(): string + + /** + * Opt the user out of data capturing and cookies/localstorage for this PostHog instance + * + * ### Usage + * + * // opt user out + * posthog.opt_out_capturing(); + * + * // opt user out with different cookie configuration from PostHog instance + * posthog.opt_out_capturing({ + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {boolean} [options.clear_persistence=true] If true, will delete all data stored by the sdk in persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this PostHog instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this PostHog instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this PostHog instance's config) + */ + static opt_out_capturing(options?: posthog.OptInOutCapturingOptions): void + + /** + * Opt the user in to data capturing and cookies/localstorage for this PostHog instance + * + * ### Usage + * + * // opt user in + * posthog.opt_in_capturing(); + * + * // opt user in with specific event name, properties, cookie configuration + * posthog.opt_in_capturing({ + * capture_event_name: 'User opted in', + * capture_event_properties: { + * 'Email': 'jdoe@example.com' + * }, + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {function} [options.capture] Function used for capturing a PostHog event to record the opt-in action (default is this PostHog instance's capture method) + * @param {string} [options.capture_event_name=$opt_in] Event name to be used for capturing the opt-in action + * @param {Object} [options.capture_properties] Set of properties to be captured along with the opt-in action + * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this PostHog instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this PostHog instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this PostHog instance's config) + */ + static opt_in_capturing(options?: posthog.OptInOutCapturingOptions): void + + /** + * Check whether the user has opted out of data capturing and cookies/localstorage for this PostHog instance + * + * ### Usage + * + * const has_opted_out = posthog.has_opted_out_capturing(); + * // use has_opted_out value + * + * @param {Object} [options] A dictionary of config options to override + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @returns {boolean} current opt-out status + */ + static has_opted_out_capturing(options?: posthog.HasOptedInOutCapturingOptions): boolean + + /** + * Check whether the user has opted in to data capturing and cookies/localstorage for this PostHog instance + * + * ### Usage + * + * const has_opted_in = posthog.has_opted_in_capturing(); + * // use has_opted_in value + * + * @param {Object} [options] A dictionary of config options to override + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @returns {boolean} current opt-in status + */ + static has_opted_in_capturing(options?: posthog.HasOptedInOutCapturingOptions): boolean + + /** + * Clear the user's opt in/out status of data capturing and cookies/localstorage for this PostHog instance + * + * ### Usage + * + * // clear user's opt-in/out status + * posthog.clear_opt_in_out_capturing(); + * + * // clear user's opt-in/out status with specific cookie configuration - should match + * // configuration used when opt_in_capturing/opt_out_capturing methods were called. + * posthog.clear_opt_in_out_capturing({ + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this PostHog instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this PostHog instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this PostHog instance's config) + */ + static clear_opt_in_out_capturing(options?: posthog.ClearOptInOutCapturingOptions): void + + /* + * See if feature flag is enabled for user. + * + * ### Usage: + * + * if(posthog.isFeatureEnabled('beta-feature')) { // do something } + * + * @param {Object|String} prop Key of the feature flag. + * @param {Object|String} options (optional) If {send_event: false}, we won't send an $feature_flag_call event to PostHog. + */ + static isFeatureEnabled(key: string, options?: posthog.isFeatureEnabledOptions): boolean + + /* + * See if feature flags are available. + * + * ### Usage: + * + * posthog.onFeatureFlags(function(featureFlags) { // do something }) + * + * @param {Function} [callback] The callback function will be called once the feature flags are ready. It'll return a list of feature flags enabled for the user. + */ + static onFeatureFlags(callback: (flags: string[]) => void): false | undefined + + /* + * Reload all feature flags for the user. + * + * ### Usage: + * + * posthog.reloadFeatureFlags() + */ + static reloadFeatureFlags(): void + + static toString(): string + + /* Will log all capture requests to the Javascript console, including event properties for easy debugging */ + static debug(): void + + /* + * Starts session recording and updates disable_session_recording to false. + * Used for manual session recording management. By default, session recording is enabled and + * starts automatically. + * + * ### Usage: + * + * posthog.startSessionRecording() + */ + static startSessionRecording(): void + + /* + * Stops session recording and updates disable_session_recording to true. + * + * ### Usage: + * + * posthog.stopSessionRecording() + */ + static stopSessionRecording(): void + + /* + * Check if session recording is currently running. + * + * ### Usage: + * + * const isSessionRecordingOn = posthog.sessionRecordingStarted() + */ + static sessionRecordingStarted(): boolean +} + +declare namespace posthog { + /* eslint-disable @typescript-eslint/no-explicit-any */ + type Property = any; + type Properties = Record; + type CaptureResult = { event: string; properties: Properties } | undefined; + type CaptureCallback = (response: any, data: any) => void; + /* eslint-enable @typescript-eslint/no-explicit-any */ + + interface Config { + api_host?: string + api_method?: string + api_transport?: string + autocapture?: boolean + rageclick?: boolean + cdn?: string + cross_subdomain_cookie?: boolean + persistence?: 'localStorage' | 'cookie' | 'memory' + persistence_name?: string + cookie_name?: string + loaded?: (posthog_instance: typeof posthog) => void + store_google?: boolean + save_referrer?: boolean + test?: boolean + verbose?: boolean + img?: boolean + capture_pageview?: boolean + debug?: boolean + cookie_expiration?: number + upgrade?: boolean + disable_session_recording?: boolean + disable_persistence?: boolean + disable_cookie?: boolean + secure_cookie?: boolean + ip?: boolean + opt_out_capturing_by_default?: boolean + opt_out_persistence_by_default?: boolean + opt_out_capturing_persistence_type?: 'localStorage' | 'cookie' + opt_out_capturing_cookie_prefix?: string | null + respect_dnt?: boolean + property_blacklist?: string[] + xhr_headers?: { [header_name: string]: string } + inapp_protocol?: string + inapp_link_new_window?: boolean + request_batching?: boolean + sanitize_properties?: (properties: posthog.Properties, event_name: string) => posthog.Properties + properties_string_max_length?: number + mask_all_element_attributes?: boolean + mask_all_text?: boolean + advanced_disable_decide?: boolean + } + + interface OptInOutCapturingOptions { + clear_persistence: boolean + persistence_type: string + cookie_prefix: string + cookie_expiration: number + cross_subdomain_cookie: boolean + secure_cookie: boolean + } + + interface HasOptedInOutCapturingOptions { + persistence_type: string + cookie_prefix: string + } + + interface ClearOptInOutCapturingOptions { + enable_persistence: boolean + persistence_type: string + cookie_prefix: string + cookie_expiration: number + cross_subdomain_cookie: boolean + secure_cookie: boolean + } + + interface isFeatureEnabledOptions { + send_event: boolean + } + + export class persistence { + static properties(): posthog.Properties + + static load(): void + + static save(): void + + static remove(): void + + static clear(): void + + /** + * @param {Object} props + * @param {*=} default_value + * @param {number=} days + */ + static register_once(props: Properties, default_value?: Property, days?: number): boolean + + /** + * @param {Object} props + * @param {number=} days + */ + static register(props: posthog.Properties, days?: number): boolean + + static unregister(prop: string): void + + static update_campaign_params(): void + + static update_search_keyword(referrer: string): void + + static update_referrer_info(referrer: string): void + + static get_referrer_info(): posthog.Properties + + static safe_merge(props: posthog.Properties): posthog.Properties + + static update_config(config: posthog.Config): void + + static set_disabled(disabled: boolean): void + + static set_cross_subdomain(cross_subdomain: boolean): void + + static get_cross_subdomain(): boolean + + static set_secure(secure: boolean): void + + static set_event_timer(event_name: string, timestamp: Date): void + + static remove_event_timer(event_name: string): Date | undefined + } + + export class people { + /* + * Set properties on a user record. + * + * ### Usage: + * + * posthog.people.set('gender', 'm'); + * + * // or set multiple properties at once + * posthog.people.set({ + * 'Company': 'Acme', + * 'Plan': 'Premium', + * 'Upgrade date': new Date() + * }); + * // properties can be strings, integers, dates, or lists + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [to] A value to set on the given property name + * @param {Function} [callback] If provided, the callback will be called after capturing the event. + */ + static set( + prop: posthog.Properties | string, + to?: posthog.Property, + callback?: posthog.CaptureCallback + ): posthog.Properties + + /* + * Set properties on a user record, only if they do not yet exist. + * This will not overwrite previous people property values, unlike + * people.set(). + * + * ### Usage: + * + * posthog.people.set_once('First Login Date', new Date()); + * + * // or set multiple properties at once + * posthog.people.set_once({ + * 'First Login Date': new Date(), + * 'Starting Plan': 'Premium' + * }); + * + * // properties can be strings, integers or dates + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [to] A value to set on the given property name + * @param {Function} [callback] If provided, the callback will be called after capturing the event. + */ + static set_once( + prop: posthog.Properties | string, + to?: posthog.Property, + callback?: posthog.CaptureCallback + ): posthog.Properties + + static toString(): string + } + + export class featureFlags { + static getFlags(): string[] + + static reloadFeatureFlags(): void + + /* + * See if feature flag is enabled for user. + * + * ### Usage: + * + * if(posthog.isFeatureEnabled('beta-feature')) { // do something } + * + * @param {Object|String} prop Key of the feature flag. + * @param {Object|String} options (optional) If {send_event: false}, we won't send an $feature_flag_call event to PostHog. + */ + static isFeatureEnabled(key: string, options?: { send_event?: boolean }): boolean + + /* + * See if feature flags are available. + * + * ### Usage: + * + * posthog.onFeatureFlags(function(featureFlags) { // do something }) + * + * @param {Function} [callback] The callback function will be called once the feature flags are ready. It'll return a list of feature flags enabled for the user. + */ + static onFeatureFlags(callback: (flags: string[]) => void): false | undefined + } + + export class feature_flags extends featureFlags {} +} + +export type PostHog = typeof posthog; + +export default posthog; diff --git a/tsconfig.json b/tsconfig.json index b139e8e8d1..b982d40b07 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,10 +22,15 @@ "es2019", "dom", "dom.iterable" - ] + ], + "paths": { + "posthog-js": [ + "./src/@types/posthog.d.ts" + ] + } }, "include": [ "./src/**/*.ts", "./src/**/*.tsx" - ] + ], } diff --git a/yarn.lock b/yarn.lock index 78f4838a09..633bf99ee6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1352,11 +1352,6 @@ tslib "^2.2.0" webcrypto-core "^1.2.0" -"@sentry/types@^6.2.2": - version "6.10.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.10.0.tgz#6b1f44e5ed4dbc2710bead24d1b32fb08daf04e1" - integrity sha512-M7s0JFgG7/6/yNVYoPUbxzaXDhnzyIQYRRJJKRaTD77YO4MHvi4Ke8alBWqD5fer0cPIfcSkBqa9BLdqRqcMWw== - "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -1453,11 +1448,6 @@ resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.1.tgz#b1b784d9e54d9879f0a8cb12f2caedab65430fe8" integrity sha512-PRuFlBBkvdDOtxlIASzTmkEFar+S66Ek48NVVTWMUjtJAdn5vyMSN8y6IZIoIymGpR36q2nZbIYazBWyFxL+IQ== -"@types/css-font-loading-module@0.0.4": - version "0.0.4" - resolved "https://registry.yarnpkg.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.4.tgz#94a835e27d1af444c65cba88523533c174463d64" - integrity sha512-ENdXf7MW4m9HeDojB2Ukbi7lYMIuQNBHVf98dbzaiG4EEJREBd6oleVAjrLRCrp7dm6CK1mmdmU9tcgF61acbw== - "@types/css-font-loading-module@^0.0.6": version "0.0.6" resolved "https://registry.yarnpkg.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.6.tgz#1ac3417ed31eeb953134d29b56bca921644b87c0" @@ -1800,11 +1790,6 @@ object.fromentries "^2.0.0" prop-types "^15.7.0" -"@xstate/fsm@^1.4.0": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@xstate/fsm/-/fsm-1.6.1.tgz#c92972b835540c4e3c5e14277f40dbcbdaee9571" - integrity sha512-xYKDNuPR36/fUK+jmhM+oauBmbdUAfuJKnDjg3/7NbN+Pj03TX7e94LXnzkwGgAR+U/HWoMqM5UPTuGIYfIx9g== - abab@^2.0.3, abab@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" @@ -3616,7 +3601,7 @@ fbjs@^0.8.4: setimmediate "^1.0.5" ua-parser-js "^0.7.18" -fflate@^0.4.1, fflate@^0.4.4: +fflate@^0.4.1: version "0.4.8" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== @@ -5654,11 +5639,6 @@ minimist@>=1.2.2, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -mitt@^1.1.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mitt/-/mitt-1.2.0.tgz#cb24e6569c806e31bd4e3995787fe38a04fdf90d" - integrity sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw== - mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" @@ -6274,7 +6254,7 @@ postcss@^8.0.2: nanoid "^3.1.23" source-map-js "^0.6.2" -posthog-js@^1.12.1: +posthog-js@1.12.1: version "1.12.1" resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.12.1.tgz#97834ee2574f34ffb5db2f5b07452c847e3c4d27" integrity sha512-Y3lzcWkS8xFY6Ryj3I4ees7qWP2WGkLw0Arcbk5xaT0+5YlA6UC2jlL/+fN9bz/Bl62EoN3BML901Cuot/QNjg== @@ -6861,22 +6841,6 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" -rrweb-snapshot@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.1.7.tgz#92a3b47b1112a1b566c2fae2edb02fa48a6f6653" - integrity sha512-+f2kCCvIQ1hbEeCWnV7mPVPDEdWEExqwcYqMd/r1nfK52QE7qU52jefUOyTe85Vy67rZGqWnfK/B25e/OTSgYg== - -rrweb@^0.9.9: - version "0.9.14" - resolved "https://registry.yarnpkg.com/rrweb/-/rrweb-0.9.14.tgz#09bec604fc44c74801e4fe910606e5a6cde008ec" - integrity sha512-nm2rrVNoyWFPrbGQmcvTTlA7XjbbgPIgO7qsW0Zyr5iOURIFJDGPHFmOVLRyLpWiriVtEoXh6a+x+D1sj+qwWg== - dependencies: - "@types/css-font-loading-module" "0.0.4" - "@xstate/fsm" "^1.4.0" - fflate "^0.4.4" - mitt "^1.1.3" - rrweb-snapshot "^1.0.3" - rst-selector-parser@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" From 7b4a7711b2c05b68fcabdae88b3674046c55036a Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 17:20:22 +0100 Subject: [PATCH 082/115] Declare return types for all public methods, even void ones --- src/PosthogAnalytics.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 6d15ca79ce..d5bb12621d 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -269,7 +269,7 @@ export class PosthogAnalytics { return this.enabled; } - public setAnonymity(anonymity: Anonymity) { + public setAnonymity(anonymity: Anonymity): void { // Update this.anonymity. // This is public for testing purposes, typically you want to call updateAnonymityFromSettings // to ensure this value is in step with the user's settings. @@ -283,17 +283,17 @@ export class PosthogAnalytics { this.anonymity = anonymity; } - public async identifyUser(userId: string) { + public async identifyUser(userId: string): Promise { if (this.anonymity == Anonymity.Pseudonymous) { this.posthog.identify(await hashHex(userId)); } } - public getAnonymity() { + public getAnonymity(): Anonymity { return this.anonymity; } - public logout() { + public logout(): void { if (this.enabled) { this.posthog.reset(); } @@ -311,7 +311,7 @@ export class PosthogAnalytics { public async trackAnonymousEvent( eventName: E["eventName"], properties: E["properties"], - ) { + ): Promise { if (this.anonymity == Anonymity.Disabled) return; await this.capture(eventName, properties); } @@ -320,7 +320,7 @@ export class PosthogAnalytics { eventName: E["eventName"], roomId: string, properties: Omit, - ) { + ): Promise { const updatedProperties = { ...properties, hashedRoomId: roomId ? await hashHex(roomId) : null, @@ -328,7 +328,7 @@ export class PosthogAnalytics { await this.trackPseudonymousEvent(eventName, updatedProperties); } - public async trackPageView(durationMs: number) { + public async trackPageView(durationMs: number): Promise { const hash = window.location.hash; let screen = null; @@ -343,7 +343,7 @@ export class PosthogAnalytics { }); } - public async updatePlatformSuperProperties() { + public async updatePlatformSuperProperties(): Promise { // Update super properties in posthog with our platform (app version, platform). // These properties will be subsequently passed in every event. // @@ -353,7 +353,7 @@ export class PosthogAnalytics { this.registerSuperProperties(this.platformSuperProperties); } - public async updateAnonymityFromSettings(userId?: string) { + public async updateAnonymityFromSettings(userId?: string): Promise { // Update this.anonymity based on the user's analytics opt-in settings // Identify the user (via hashed user ID) to posthog if anonymity is pseudonmyous this.setAnonymity(PosthogAnalytics.getAnonymityFromSettings()); From d401789f9ec3e284053bd5d8b00bf0723582c469 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 17:26:24 +0100 Subject: [PATCH 083/115] Ignore eslint conventions in disastrous posthog type definitions --- src/@types/posthog.d.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/@types/posthog.d.ts b/src/@types/posthog.d.ts index 1108e2c6df..1ca475cd3b 100644 --- a/src/@types/posthog.d.ts +++ b/src/@types/posthog.d.ts @@ -1,3 +1,12 @@ +// A clone of the type definitions from posthog-js, stripped of references to transitive +// dependencies which we don't actually use, so that we don't need to install them. +// +// Original file lives in node_modules/posthog/dist/module.d.ts + +/* eslint-disable @typescript-eslint/member-delimiter-style */ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable camelcase */ + // Type definitions for exported methods declare class posthog { From 07eaee25d29542a4fce9d02a976751ffc3e9c9a3 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 28 Jul 2021 17:54:35 +0100 Subject: [PATCH 084/115] Default properties to {} to avoid passing it superfluously for events with no properties --- src/PosthogAnalytics.ts | 4 ++-- src/components/views/auth/Welcome.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index d5bb12621d..c8e156f2fa 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -302,7 +302,7 @@ export class PosthogAnalytics { public async trackPseudonymousEvent( eventName: E["eventName"], - properties: E["properties"], + properties: E["properties"] = {}, ) { if (this.anonymity == Anonymity.Anonymous || this.anonymity == Anonymity.Disabled) return; await this.capture(eventName, properties); @@ -310,7 +310,7 @@ export class PosthogAnalytics { public async trackAnonymousEvent( eventName: E["eventName"], - properties: E["properties"], + properties: E["properties"] = {}, ): Promise { if (this.anonymity == Anonymity.Disabled) return; await this.capture(eventName, properties); diff --git a/src/components/views/auth/Welcome.tsx b/src/components/views/auth/Welcome.tsx index 75bbe15411..7c405b0835 100644 --- a/src/components/views/auth/Welcome.tsx +++ b/src/components/views/auth/Welcome.tsx @@ -76,6 +76,6 @@ export default class Welcome extends React.PureComponent { } componentDidMount() { - getAnalytics().trackAnonymousEvent("welcome_screen_load", {}); + getAnalytics().trackAnonymousEvent("welcome_screen_load"); } } From ae647658706a87c352b7e120423ebc87f33d8dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 29 Jul 2021 08:45:32 +0200 Subject: [PATCH 085/115] playMedia only if necessary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/VideoFeed.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 9975f70d62..af2fd92016 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -22,6 +22,7 @@ import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed'; import { logger } from 'matrix-js-sdk/src/logger'; import MemberAvatar from "../avatars/MemberAvatar"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { objectHasDiff } from '../../../utils/objects'; interface IProps { call: MatrixCall; @@ -46,7 +47,6 @@ interface IState { videoMuted: boolean; } -// TODO: We shouldn't be calling playMedia() all the time @replaceableComponent("views.voip.VideoFeed") export default class VideoFeed extends React.PureComponent { private element: HTMLVideoElement; @@ -69,8 +69,10 @@ export default class VideoFeed extends React.PureComponent { this.updateFeed(this.props.feed, null); } - componentDidUpdate(prevProps: IProps) { + componentDidUpdate(prevProps: IProps, prevState: IState) { this.updateFeed(prevProps.feed, this.props.feed); + // If the mutes state has changed, we try to playMedia() + if (prevState.videoMuted !== this.state.videoMuted) this.playMedia(); } static getDerivedStateFromProps(props: IProps) { @@ -142,7 +144,7 @@ export default class VideoFeed extends React.PureComponent { } private onNewStream = async () => { - await this.setState({ + this.setState({ audioMuted: this.props.feed.isAudioMuted(), videoMuted: this.props.feed.isVideoMuted(), }); @@ -150,11 +152,10 @@ export default class VideoFeed extends React.PureComponent { }; private onMuteStateChanged = async () => { - await this.setState({ + this.setState({ audioMuted: this.props.feed.isAudioMuted(), videoMuted: this.props.feed.isVideoMuted(), }); - this.playMedia(); }; private onResize = (e) => { From 152168ef2ddc99a598fcebbbd517560e053fa850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 29 Jul 2021 10:20:59 +0200 Subject: [PATCH 086/115] Add mic mute icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/img/voip/mic-muted.svg | 5 +++++ res/img/voip/mic-unmuted.svg | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 res/img/voip/mic-muted.svg create mode 100644 res/img/voip/mic-unmuted.svg diff --git a/res/img/voip/mic-muted.svg b/res/img/voip/mic-muted.svg new file mode 100644 index 0000000000..0cb7ad1c9e --- /dev/null +++ b/res/img/voip/mic-muted.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/voip/mic-unmuted.svg b/res/img/voip/mic-unmuted.svg new file mode 100644 index 0000000000..8334cafa0a --- /dev/null +++ b/res/img/voip/mic-unmuted.svg @@ -0,0 +1,4 @@ + + + + From cb89dd408c260a14bd9726fd65fe3f6f5acf2ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 29 Jul 2021 15:05:26 +0200 Subject: [PATCH 087/115] Use mic mute icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/voip/_CallView.scss | 10 +++- res/css/views/voip/_CallViewSidebar.scss | 11 +++++ res/css/views/voip/_VideoFeed.scss | 48 +++++++++++++++--- src/components/views/voip/VideoFeed.tsx | 63 ++++++++++++++++-------- 4 files changed, 103 insertions(+), 29 deletions(-) diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 104e2993d8..eff865f20c 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -76,16 +76,22 @@ limitations under the License. &.mx_VideoFeed_voice { // We don't want to collide with the call controls that have 52px of height - padding-bottom: 52px; + margin-bottom: 52px; background-color: $inverted-bg-color; display: flex; justify-content: center; align-items: center; } - &.mx_VideoFeed_video { + .mx_VideoFeed_video { + height: 100%; background-color: #000; } + + .mx_VideoFeed_mic { + left: 10px; + bottom: 10px; + } } } diff --git a/res/css/views/voip/_CallViewSidebar.scss b/res/css/views/voip/_CallViewSidebar.scss index 79bf3cbf09..892a137a32 100644 --- a/res/css/views/voip/_CallViewSidebar.scss +++ b/res/css/views/voip/_CallViewSidebar.scss @@ -35,12 +35,23 @@ limitations under the License. width: 100%; &.mx_VideoFeed_voice { + border-radius: 4px; + display: flex; align-items: center; justify-content: center; aspect-ratio: 16 / 9; } + + .mx_VideoFeed_video { + border-radius: 4px; + } + + .mx_VideoFeed_mic { + left: 6px; + bottom: 6px; + } } &.mx_CallViewSidebar_pipMode { diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss index 07a4a0e530..3a0f62636e 100644 --- a/res/css/views/voip/_VideoFeed.scss +++ b/res/css/views/voip/_VideoFeed.scss @@ -15,18 +15,52 @@ limitations under the License. */ .mx_VideoFeed { - border-radius: 4px; - + overflow: hidden; + position: relative; &.mx_VideoFeed_voice { background-color: $inverted-bg-color; } - &.mx_VideoFeed_video { + .mx_VideoFeed_video { + width: 100%; background-color: transparent; + + &.mx_VideoFeed_video_mirror { + transform: scale(-1, 1); + } + } + + .mx_VideoFeed_mic { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + + width: 24px; + height: 24px; + + background-color: rgba(0, 0, 0, 0.5); // Same on both themes + border-radius: 100%; + + &::before { + position: absolute; + content: ""; + width: 16px; + height: 16px; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + background-color: white; // Same on both themes + border-radius: 7px; + } + + &.mx_VideoFeed_mic_muted::before { + mask-image: url('$(res)/img/voip/mic-muted.svg'); + } + + &.mx_VideoFeed_mic_unmuted::before { + mask-image: url('$(res)/img/voip/mic-unmuted.svg'); + } } } - -.mx_VideoFeed_mirror { - transform: scale(-1, 1); -} diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index af2fd92016..09d0c97a0d 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -22,7 +22,7 @@ import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed'; import { logger } from 'matrix-js-sdk/src/logger'; import MemberAvatar from "../avatars/MemberAvatar"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { objectHasDiff } from '../../../utils/objects'; +import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes'; interface IProps { call: MatrixCall; @@ -165,39 +165,62 @@ export default class VideoFeed extends React.PureComponent { }; render() { - const videoClasses = { - mx_VideoFeed: true, + const { pipMode, primary, feed } = this.props; + + const wrapperClasses = classnames("mx_VideoFeed", { mx_VideoFeed_voice: this.state.videoMuted, - mx_VideoFeed_video: !this.state.videoMuted, - mx_VideoFeed_mirror: ( - this.props.feed.isLocal() && - SettingsStore.getValue('VideoView.flipVideoHorizontally') - ), - }; + }); + const micIconClasses = classnames("mx_VideoFeed_mic", { + mx_VideoFeed_mic_muted: this.state.audioMuted, + mx_VideoFeed_mic_unmuted: !this.state.audioMuted, + }); - const { pipMode, primary } = this.props; + let micIcon; + if ( + feed.purpose !== SDPStreamMetadataPurpose.Screenshare && + !pipMode && + !feed.isLocal() + ) { + micIcon = ( +
+ ); + } + let content; if (this.state.videoMuted) { const member = this.props.feed.getMember(); + let avatarSize; if (pipMode && primary) avatarSize = 76; else if (pipMode && !primary) avatarSize = 16; else if (!pipMode && primary) avatarSize = 160; else; // TBD - return ( -
- -
+ content =( + ); } else { - return ( -
; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index c837e814c8..ba6bb26cbf 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -1381,8 +1381,8 @@ const BasicUserInfo: React.FC<{ className="mx_UserInfo_field" onClick={() => { dis.dispatch({ - action: Action.ViewUserSettings, - initialTabId: UserTab.Security, + action: Action.ViewUserSettings, + initialTabId: UserTab.Security, }); }} > diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index cdca9ab75d..674bcdaec2 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -64,9 +64,9 @@ const NewRoomIntro = () => { height={AVATAR_SIZE} onClick={() => { defaultDispatcher.dispatch({ - action: Action.ViewUser, - // XXX: We should be using a real member object and not assuming what the receiver wants. - member: member || { userId: dmPartner } as User, + action: Action.ViewUser, + // XXX: We should be using a real member object and not assuming what the receiver wants. + member: member || { userId: dmPartner } as User, }); }} /> diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js index 4ea16f69a3..c9688b4d29 100644 --- a/src/components/views/rooms/ReadReceiptMarker.js +++ b/src/components/views/rooms/ReadReceiptMarker.js @@ -145,7 +145,7 @@ export default class ReadReceiptMarker extends React.PureComponent { if (oldInfo && oldInfo.left) { // start at the old height and in the old h pos startStyles.push({ top: startTopOffset+"px", - left: toPx(oldInfo.left) }); + left: toPx(oldInfo.left) }); } startStyles.push({ top: startTopOffset+'px', left: '0' }); @@ -174,14 +174,14 @@ export default class ReadReceiptMarker extends React.PureComponent { title = _t( "Seen by %(userName)s at %(dateTime)s", { userName: this.props.fallbackUserId, - dateTime: dateString }, + dateTime: dateString }, ); } else { title = _t( "Seen by %(displayName)s (%(userName)s) at %(dateTime)s", { displayName: this.props.member.rawDisplayName, - userName: this.props.fallbackUserId, - dateTime: dateString }, + userName: this.props.fallbackUserId, + dateTime: dateString }, ); } } diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index e2d51f13a4..dcb895f09e 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -106,7 +106,7 @@ describe('MemberEventListSummary', function() { const result = wrapper.props.children; expect(result.props.children).toEqual([ -
Expanded membership
, +
Expanded membership
, ]); }); @@ -129,8 +129,8 @@ describe('MemberEventListSummary', function() { const result = wrapper.props.children; expect(result.props.children).toEqual([ -
Expanded membership
, -
Expanded membership
, +
Expanded membership
, +
Expanded membership
, ]); }); diff --git a/test/notifications/ContentRules-test.js b/test/notifications/ContentRules-test.js index 9c21c05da7..2b18a18488 100644 --- a/test/notifications/ContentRules-test.js +++ b/test/notifications/ContentRules-test.js @@ -56,7 +56,7 @@ describe("ContentRules", function() { describe("parseContentRules", function() { it("should handle there being no keyword rules", function() { const rules = { 'global': { 'content': [ - USERNAME_RULE, + USERNAME_RULE, ] } }; const parsed = ContentRules.parseContentRules(rules); expect(parsed.rules).toEqual([]); diff --git a/test/test-utils.js b/test/test-utils.js index 5e29fd242e..217c399443 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -129,8 +129,8 @@ export function mkEvent(opts) { if (opts.skey) { event.state_key = opts.skey; } else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules", - "m.room.power_levels", "m.room.topic", "m.room.history_visibility", "m.room.encryption", - "com.example.state"].indexOf(opts.type) !== -1) { + "m.room.power_levels", "m.room.topic", "m.room.history_visibility", "m.room.encryption", + "com.example.state"].indexOf(opts.type) !== -1) { event.state_key = ""; } return opts.event ? new MatrixEvent(event) : event; diff --git a/test/utils/ShieldUtils-test.js b/test/utils/ShieldUtils-test.js index b70031dc21..85f9de3150 100644 --- a/test/utils/ShieldUtils-test.js +++ b/test/utils/ShieldUtils-test.js @@ -49,7 +49,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() { it.each( [[true, true], [true, false], - [false, true], [false, false]], + [false, true], [false, false]], )("2 unverified: returns 'normal', self-trust = %s, DM = %s", async (trusted, dm) => { const client = mkClient(trusted); const room = { @@ -62,7 +62,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() { it.each( [["verified", true, true], ["verified", true, false], - ["verified", false, true], ["warning", false, false]], + ["verified", false, true], ["warning", false, false]], )("2 verified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => { const client = mkClient(trusted); const room = { @@ -75,7 +75,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() { it.each( [["normal", true, true], ["normal", true, false], - ["normal", false, true], ["warning", false, false]], + ["normal", false, true], ["warning", false, false]], )("2 mixed: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => { const client = mkClient(trusted); const room = { @@ -88,7 +88,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() { it.each( [["verified", true, true], ["verified", true, false], - ["warning", false, true], ["warning", false, false]], + ["warning", false, true], ["warning", false, false]], )("0 others: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => { const client = mkClient(trusted); const room = { @@ -101,7 +101,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() { it.each( [["verified", true, true], ["verified", true, false], - ["verified", false, true], ["verified", false, false]], + ["verified", false, true], ["verified", false, false]], )("1 verified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => { const client = mkClient(trusted); const room = { @@ -114,7 +114,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() { it.each( [["normal", true, true], ["normal", true, false], - ["normal", false, true], ["normal", false, false]], + ["normal", false, true], ["normal", false, false]], )("1 unverified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => { const client = mkClient(trusted); const room = { diff --git a/yarn.lock b/yarn.lock index 2a03f640ee..f121112a1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3233,9 +3233,9 @@ eslint-config-google@^0.14.0: resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a" integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw== -"eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#main": - version "0.3.4" - resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/45f6937539192e3820edcafc4d6d4d4187e85a6a" +"eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945": + version "0.3.5" + resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/2306b3d4da4eba908b256014b979f1d3d43d2945" eslint-plugin-react-hooks@^4.2.0: version "4.2.0" From 612384a6a36bf468cc4575d8fccb9d1cbf941ff0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 3 Aug 2021 18:03:46 +0100 Subject: [PATCH 105/115] Switch to new changelog generator allchange is typescript so has a 'prepare' script to tsc it into javascript so it can be a binscript - hopefully this won't cause it to make too much of a pain of itself causing tsc to run on every yarn add/install --- package.json | 1 + yarn.lock | 311 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 309 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 704315ca69..fcb1e9133c 100644 --- a/package.json +++ b/package.json @@ -147,6 +147,7 @@ "@typescript-eslint/eslint-plugin": "^4.17.0", "@typescript-eslint/parser": "^4.17.0", "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1", + "allchange": "github:matrix-org/allchange", "babel-jest": "^26.6.3", "chokidar": "^3.5.1", "concurrently": "^5.3.0", diff --git a/yarn.lock b/yarn.lock index f121112a1b..2b504d2366 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1324,6 +1324,107 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@octokit/auth-token@^2.4.4": + version "2.4.5" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.5.tgz#568ccfb8cb46f36441fac094ce34f7a875b197f3" + integrity sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA== + dependencies: + "@octokit/types" "^6.0.3" + +"@octokit/core@^3.5.0": + version "3.5.1" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b" + integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw== + dependencies: + "@octokit/auth-token" "^2.4.4" + "@octokit/graphql" "^4.5.8" + "@octokit/request" "^5.6.0" + "@octokit/request-error" "^2.0.5" + "@octokit/types" "^6.0.3" + before-after-hook "^2.2.0" + universal-user-agent "^6.0.0" + +"@octokit/endpoint@^6.0.1": + version "6.0.12" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.12.tgz#3b4d47a4b0e79b1027fb8d75d4221928b2d05658" + integrity sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA== + dependencies: + "@octokit/types" "^6.0.3" + is-plain-object "^5.0.0" + universal-user-agent "^6.0.0" + +"@octokit/graphql@^4.5.8": + version "4.6.4" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.6.4.tgz#0c3f5bed440822182e972317122acb65d311a5ed" + integrity sha512-SWTdXsVheRmlotWNjKzPOb6Js6tjSqA2a8z9+glDJng0Aqjzti8MEWOtuT8ZSu6wHnci7LZNuarE87+WJBG4vg== + dependencies: + "@octokit/request" "^5.6.0" + "@octokit/types" "^6.0.3" + universal-user-agent "^6.0.0" + +"@octokit/openapi-types@^9.3.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.3.0.tgz#160347858d727527901c6aae7f7d5c2414cc1f2e" + integrity sha512-oz60hhL+mDsiOWhEwrj5aWXTOMVtQgcvP+sRzX4C3cH7WOK9QSAoEtjWh0HdOf6V3qpdgAmUMxnQPluzDWR7Fw== + +"@octokit/plugin-paginate-rest@^2.6.2": + version "2.15.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.15.0.tgz#9c956c3710b2bd786eb3814eaf5a2b17392c150d" + integrity sha512-/vjcb0w6ggVRtsb8OJBcRR9oEm+fpdo8RJk45khaWw/W0c8rlB2TLCLyZt/knmC17NkX7T9XdyQeEY7OHLSV1g== + dependencies: + "@octokit/types" "^6.23.0" + +"@octokit/plugin-request-log@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" + integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== + +"@octokit/plugin-rest-endpoint-methods@5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.6.0.tgz#c28833b88d0f07bf94093405d02d43d73c7de99b" + integrity sha512-2G7lIPwjG9XnTlNhe/TRnpI8yS9K2l68W4RP/ki3wqw2+sVeTK8hItPxkqEI30VeH0UwnzpuksMU/yHxiVVctw== + dependencies: + "@octokit/types" "^6.23.0" + deprecation "^2.3.1" + +"@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677" + integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg== + dependencies: + "@octokit/types" "^6.0.3" + deprecation "^2.0.0" + once "^1.4.0" + +"@octokit/request@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.0.tgz#6084861b6e4fa21dc40c8e2a739ec5eff597e672" + integrity sha512-4cPp/N+NqmaGQwbh3vUsYqokQIzt7VjsgTYVXiwpUP2pxd5YiZB2XuTedbb0SPtv9XS7nzAKjAuQxmY8/aZkiA== + dependencies: + "@octokit/endpoint" "^6.0.1" + "@octokit/request-error" "^2.1.0" + "@octokit/types" "^6.16.1" + is-plain-object "^5.0.0" + node-fetch "^2.6.1" + universal-user-agent "^6.0.0" + +"@octokit/rest@^18.6.7": + version "18.8.0" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.8.0.tgz#ba24f7ba554f015a7ae2b7cc2aecef5386ddfea5" + integrity sha512-lsuNRhgzGnLMn/NmQTNCit/6jplFWiTUlPXhqN0zCMLwf2/9pseHzsnTW+Cjlp4bLMEJJNPa5JOzSLbSCOahKw== + dependencies: + "@octokit/core" "^3.5.0" + "@octokit/plugin-paginate-rest" "^2.6.2" + "@octokit/plugin-request-log" "^1.0.2" + "@octokit/plugin-rest-endpoint-methods" "5.6.0" + +"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.23.0": + version "6.23.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.23.0.tgz#b39f242b20036e89fa8f34f7962b4e9b7ff8f65b" + integrity sha512-eG3clC31GSS7K3oBK6C6o7wyXPrkP+mu++eus8CSZdpRytJ5PNszYxudOQ0spWZQ3S9KAtoTG6v1WK5prJcJrA== + dependencies: + "@octokit/openapi-types" "^9.3.0" + "@peculiar/asn1-schema@^2.0.27", "@peculiar/asn1-schema@^2.0.32": version "2.0.37" resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.0.37.tgz#700476512ab903d809f64a3040fb1b2fe6fb6d4b" @@ -1850,6 +1951,17 @@ ajv@^8.0.1: require-from-string "^2.0.2" uri-js "^4.2.2" +"allchange@github:matrix-org/allchange": + version "0.0.1" + resolved "https://codeload.github.com/matrix-org/allchange/tar.gz/56b37b06339a3ac3fe771f3ec3d0bff798df8dab" + dependencies: + "@octokit/rest" "^18.6.7" + cli-color "^2.0.0" + js-yaml "^4.1.0" + loglevel "^1.7.1" + semver "^7.3.5" + yargs "^17.0.1" + another-json@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/another-json/-/another-json-0.2.0.tgz#b5f4019c973b6dd5c6506a2d93469cb6d32aeedc" @@ -1867,6 +1979,11 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.21.3" +ansi-regex@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + ansi-regex@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" @@ -1914,6 +2031,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -2206,6 +2328,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +before-after-hook@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e" + integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ== + binary-extensions@^1.0.0: version "1.13.1" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" @@ -2506,6 +2633,18 @@ classnames@*, classnames@^2.2.6: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== +cli-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-2.0.0.tgz#11ecfb58a79278cf6035a60c54e338f9d837897c" + integrity sha512-a0VZ8LeraW0jTuCkuAGMNufareGHhyZU9z8OGsW0gXd1hZGi1SRuNRXdbGkraBBKnhyUhyebFWnRbp+dIn0f0A== + dependencies: + ansi-regex "^2.1.1" + d "^1.0.1" + es5-ext "^0.10.51" + es6-iterator "^2.0.3" + memoizee "^0.4.14" + timers-ext "^0.1.7" + cliui@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" @@ -2524,6 +2663,15 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + clone-deep@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" @@ -2788,6 +2936,14 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340" integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw== +d@1, d@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -2895,6 +3051,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +deprecation@^2.0.0, deprecation@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -3191,6 +3352,42 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.51, es5-ext@^0.10.53, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: + version "0.10.53" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" + integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q== + dependencies: + es6-iterator "~2.0.3" + es6-symbol "~3.1.3" + next-tick "~1.0.0" + +es6-iterator@^2.0.3, es6-iterator@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" + integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + dependencies: + d "^1.0.1" + ext "^1.1.2" + +es6-weak-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" + integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== + dependencies: + d "1" + es5-ext "^0.10.46" + es6-iterator "^2.0.3" + es6-symbol "^3.1.1" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -3383,6 +3580,14 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= + dependencies: + d "1" + es5-ext "~0.10.14" + events@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -3478,6 +3683,13 @@ expect@^26.6.2: jest-message-util "^26.6.2" jest-regex-util "^26.0.0" +ext@^1.1.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" + integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A== + dependencies: + type "^2.0.0" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -3783,7 +3995,7 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-caller-file@^2.0.1: +get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -4502,6 +4714,11 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== +is-promise@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" + integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== + is-regex@^1.0.3, is-regex@^1.0.5, is-regex@^1.1.2, is-regex@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f" @@ -5122,6 +5339,13 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -5396,6 +5620,13 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" + integrity sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM= + dependencies: + es5-ext "~0.10.2" + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -5528,6 +5759,20 @@ memoize-one@^5.1.1: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== +memoizee@^0.4.14: + version "0.4.15" + resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.15.tgz#e6f3d2da863f318d02225391829a6c5956555b72" + integrity sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ== + dependencies: + d "^1.0.1" + es5-ext "^0.10.53" + es6-weak-map "^2.0.3" + event-emitter "^0.3.5" + is-promise "^2.2.2" + lru-queue "^0.1.0" + next-tick "^1.1.0" + timers-ext "^0.1.7" + meow@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364" @@ -5701,12 +5946,22 @@ nearley@^2.7.10: railroad-diagrams "^1.0.0" randexp "0.4.6" +next-tick@1, next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + +next-tick@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" + integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -node-fetch@2.6.1: +node-fetch@2.6.1, node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== @@ -7498,6 +7753,14 @@ throat@^5.0.0: resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== +timers-ext@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6" + integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ== + dependencies: + es5-ext "~0.10.46" + next-tick "1" + tiny-invariant@^1.0.6: version "1.1.0" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" @@ -7657,6 +7920,16 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== + +type@^2.0.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d" + integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw== + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -7753,6 +8026,11 @@ unist-util-stringify-position@^2.0.0: dependencies: "@types/unist" "^2.0.2" +universal-user-agent@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" + integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== + universalify@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -8056,6 +8334,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -8091,6 +8378,11 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" @@ -8117,7 +8409,7 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.2.3: +yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== @@ -8155,6 +8447,19 @@ yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^17.0.1: + version "17.0.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.0.1.tgz#6a1ced4ed5ee0b388010ba9fd67af83b9362e0bb" + integrity sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" From 82c644564870090dc9293bdef86b5fb69d0f6dad Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 30 Jul 2021 21:35:11 +0100 Subject: [PATCH 106/115] Add release_config for changelogging --- release_config.yaml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 release_config.yaml diff --git a/release_config.yaml b/release_config.yaml new file mode 100644 index 0000000000..12e857cbdd --- /dev/null +++ b/release_config.yaml @@ -0,0 +1,4 @@ +subprojects: + matrix-js-sdk: + includeByDefault: false + From 7fc632d0cfc25e45d31a164c0e22179b1effe782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 4 Aug 2021 08:42:07 +0200 Subject: [PATCH 107/115] Put font names into "" so that stylelint shuts up about them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/themes/legacy-light/css/_legacy-light.scss | 5 +++-- res/themes/light/css/_light.scss | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index b7d45452ff..b37b4300c9 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -8,9 +8,10 @@ /* Noto Color Emoji contains digits, in fixed-width, therefore causing digits in flowed text to stand out. TODO: Consider putting all emoji fonts to the end rather than the front. */ -$font-family: Nunito, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Arial, Helvetica, Sans-Serif, 'Noto Color Emoji'; +$font-family: 'Nunito', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial', 'Helvetica', 'Sans-Serif', 'Noto Color Emoji'; + +$monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', 'monospace', 'Noto Color Emoji'; -$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Courier, monospace, 'Noto Color Emoji'; // unified palette // try to use these colors when possible diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 32722515d8..db3aa95442 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -8,9 +8,9 @@ /* Noto Color Emoji contains digits, in fixed-width, therefore causing digits in flowed text to stand out. TODO: Consider putting all emoji fonts to the end rather than the front. */ -$font-family: Inter, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Arial, Helvetica, Sans-Serif, 'Noto Color Emoji'; +$font-family: 'Inter', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial', 'Helvetica', 'Sans-Serif', 'Noto Color Emoji'; -$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Courier, monospace, 'Noto Color Emoji'; +$monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', 'monospace', 'Noto Color Emoji'; // unified palette // try to use these colors when possible From 050e260655ba1399cff0f835eba0da35a2f5360d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 4 Aug 2021 08:45:58 +0200 Subject: [PATCH 108/115] Fix weird spacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/themes/legacy-light/css/_legacy-light.scss | 2 +- res/themes/light/css/_light.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index b37b4300c9..c784f18a2b 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -391,7 +391,7 @@ $eventbubble-reply-color: #C1C6CD; @define-mixin mx_DialogButton_secondary { // flip colours for the secondary ones font-weight: 600; - border: 1px solid $accent-color ! important; + border: 1px solid $accent-color !important; color: $accent-color; background-color: $button-secondary-bg-color; } diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index db3aa95442..1412b900f9 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -392,7 +392,7 @@ $eventbubble-reply-color: #C1C6CD; @define-mixin mx_DialogButton_secondary { // flip colours for the secondary ones font-weight: 600; - border: 1px solid $accent-color ! important; + border: 1px solid $accent-color !important; color: $accent-color; background-color: $button-secondary-bg-color; } From ca94518de5e3263f20f7778995bdb481e3ce82cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 4 Aug 2021 08:46:09 +0200 Subject: [PATCH 109/115] Improve bubble colors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/themes/dark/css/_dark.scss | 17 ++++++++++------- res/themes/legacy-light/css/_legacy-light.scss | 12 +++++++----- res/themes/light/css/_light.scss | 15 +++++++++------ 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 7b83fe0cb2..6adc821487 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -1,3 +1,6 @@ +// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0 +$system-dark: #21262C; + // unified palette // try to use these colors when possible $bg-color: #15191E; @@ -47,7 +50,7 @@ $inverted-bg-color: $base-color; $selected-color: $room-highlight-color; // selected for hoverover & selected event tiles -$event-selected-color: #21262c; +$event-selected-color: $system-dark; // used for the hairline dividers in RoomView $primary-hairline-color: transparent; @@ -91,7 +94,7 @@ $lightbox-background-bg-color: #000; $lightbox-background-bg-opacity: 0.85; $settings-grey-fg-color: #a2a2a2; -$settings-profile-placeholder-bg-color: #21262c; +$settings-profile-placeholder-bg-color: $system-dark; $settings-profile-overlay-placeholder-fg-color: #454545; $settings-profile-button-bg-color: #e7e7e7; $settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color; @@ -175,7 +178,7 @@ $button-link-bg-color: transparent; $togglesw-off-color: $room-highlight-color; $progressbar-fg-color: $accent-color; -$progressbar-bg-color: #21262c; +$progressbar-bg-color: $system-dark; $visual-bell-bg-color: #800; @@ -210,7 +213,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color; $message-body-panel-fg-color: $secondary-fg-color; $message-body-panel-bg-color: #394049; // "Dark Tile" $message-body-panel-icon-fg-color: $secondary-fg-color; -$message-body-panel-icon-bg-color: #21262C; // "System Dark" +$message-body-panel-icon-bg-color: $system-dark; // "System Dark" $voice-record-stop-border-color: $quaternary-fg-color; $voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; @@ -228,9 +231,9 @@ $groupFilterPanel-background-blur-amount: 30px; $composer-shadow-color: rgba(0, 0, 0, 0.28); // Bubble tiles -$eventbubble-self-bg: #143A34; -$eventbubble-others-bg: #394049; -$eventbubble-bg-hover: $header-panel-bg-color; +$eventbubble-self-bg: #14322E; +$eventbubble-others-bg: $event-selected-color; +$eventbubble-bg-hover: #1C2026; $eventbubble-avatar-outline: $bg-color; $eventbubble-reply-color: #C1C6CD; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index c784f18a2b..e9bfe46c89 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -12,6 +12,8 @@ $font-family: 'Nunito', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial $monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', 'monospace', 'Noto Color Emoji'; +// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0 +$system-light: #F4F6FA; // unified palette // try to use these colors when possible @@ -180,7 +182,7 @@ $composer-e2e-icon-color: #91a1c0; $header-divider-color: #91a1c0; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #F4F6FA; +$voipcall-plinth-color: $system-light; // ******************** @@ -332,7 +334,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color; $message-body-panel-fg-color: $secondary-fg-color; $message-body-panel-bg-color: #E3E8F0; $message-body-panel-icon-fg-color: $secondary-fg-color; -$message-body-panel-icon-bg-color: #F4F6FA; +$message-body-panel-icon-bg-color: $system-light; // See non-legacy _light for variable information $voice-record-stop-symbol-color: #ff4b55; @@ -349,9 +351,9 @@ $appearance-tab-border-color: $input-darker-bg-color; $composer-shadow-color: tranparent; // Bubble tiles -$eventbubble-self-bg: #F8FDFC; -$eventbubble-others-bg: #F7F8F9; -$eventbubble-bg-hover: rgb(242, 242, 242); +$eventbubble-self-bg: #F0FBF8; +$eventbubble-others-bg: $system-light; +$eventbubble-bg-hover: #FAFBFD; $eventbubble-avatar-outline: #fff; $eventbubble-reply-color: #C1C6CD; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 1412b900f9..90ae808adc 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -12,6 +12,9 @@ $font-family: 'Inter', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial' $monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', 'monospace', 'Noto Color Emoji'; +// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0 +$system-light: #F4F6FA; + // unified palette // try to use these colors when possible $accent-color: #0DBD8B; @@ -138,7 +141,7 @@ $blockquote-bar-color: #ddd; $blockquote-fg-color: #777; $settings-grey-fg-color: #a2a2a2; -$settings-profile-placeholder-bg-color: #f4f6fa; +$settings-profile-placeholder-bg-color: $system-light; $settings-profile-overlay-placeholder-fg-color: #2e2f32; $settings-profile-button-bg-color: #e7e7e7; $settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color; @@ -168,7 +171,7 @@ $composer-e2e-icon-color: #91A1C0; $header-divider-color: #91A1C0; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #F4F6FA; +$voipcall-plinth-color: $system-light; // ******************** @@ -327,7 +330,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color; $message-body-panel-fg-color: $secondary-fg-color; $message-body-panel-bg-color: #E3E8F0; // "Separator" $message-body-panel-icon-fg-color: $secondary-fg-color; -$message-body-panel-icon-bg-color: #F4F6FA; +$message-body-panel-icon-bg-color: $system-light; // These two don't change between themes. They are the $warning-color, but we don't // want custom themes to affect them by accident. @@ -350,9 +353,9 @@ $groupFilterPanel-background-blur-amount: 20px; $composer-shadow-color: rgba(0, 0, 0, 0.04); // Bubble tiles -$eventbubble-self-bg: #F8FDFC; -$eventbubble-others-bg: #F7F8F9; -$eventbubble-bg-hover: #FEFCF5; +$eventbubble-self-bg: #F0FBF8; +$eventbubble-others-bg: $system-light; +$eventbubble-bg-hover: #FAFBFD; $eventbubble-avatar-outline: $primary-bg-color; $eventbubble-reply-color: #C1C6CD; From 4b78edd65241256351e68b799b05c607de5bd574 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 4 Aug 2021 09:30:04 +0100 Subject: [PATCH 110/115] Remove patch-package and postinstall-postinstall --- package.json | 5 +- patches/posthog-js+1.12.2.patch | 78 --------------------------- yarn.lock | 96 ++------------------------------- 3 files changed, 6 insertions(+), 173 deletions(-) delete mode 100644 patches/posthog-js+1.12.2.patch diff --git a/package.json b/package.json index e7ea85ccd4..8f5459ab99 100644 --- a/package.json +++ b/package.json @@ -51,8 +51,7 @@ "lint:style": "stylelint 'res/css/**/*.scss'", "test": "jest", "test:e2e": "./test/end-to-end-tests/run.sh --app-url http://localhost:8080", - "coverage": "yarn test --coverage", - "postinstall": "patch-package" + "coverage": "yarn test --coverage" }, "dependencies": { "@babel/runtime": "^7.12.5", @@ -166,8 +165,6 @@ "matrix-mock-request": "^1.2.3", "matrix-react-test-utils": "^0.2.3", "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", - "patch-package": "^6.4.7", - "postinstall-postinstall": "^2.1.0", "react-test-renderer": "^17.0.2", "rimraf": "^3.0.2", "stylelint": "^13.9.0", diff --git a/patches/posthog-js+1.12.2.patch b/patches/posthog-js+1.12.2.patch deleted file mode 100644 index 430a9f0a62..0000000000 --- a/patches/posthog-js+1.12.2.patch +++ /dev/null @@ -1,78 +0,0 @@ -diff --git a/node_modules/posthog-js/dist/module.d.ts b/node_modules/posthog-js/dist/module.d.ts -index d3cba02..8c87ef8 100644 ---- a/node_modules/posthog-js/dist/module.d.ts -+++ b/node_modules/posthog-js/dist/module.d.ts -@@ -1,7 +1,5 @@ - // Type definitions for exported methods -- --import { EventProcessor, Integration } from '@sentry/types' --import { MaskInputOptions, SlimDOMOptions } from 'rrweb-snapshot' -+// Patched to remove references to sentry and rrweb-snapshot which aren't otherwise SDK dependencies - - declare class posthog { - /** -@@ -485,25 +483,6 @@ declare class posthog { - */ - static reloadFeatureFlags(): void - -- /** -- * Integrate Sentry with PostHog. This will add a direct link to the person in Sentry, and an $exception event in PostHog -- * -- * ### Usage -- * -- * Sentry.init({ -- * dsn: 'https://example', -- * integrations: [ -- * new posthog.SentryIntegration(posthog) -- * ] -- * }) -- * -- * @param {Object} [posthog] The posthog object -- * @param {string} [organization] Optional: The Sentry organization, used to send a direct link from PostHog to Sentry -- * @param {Number} [projectId] Optional: The Sentry project id, used to send a direct link from PostHog to Sentry -- * @param {string} [prefix] Optional: Url of a self-hosted sentry instance (default: https://sentry.io/organizations/) -- */ -- static SentryIntegration: typeof SentryIntegration -- - static toString(): string - - /* Will log all capture requests to the Javascript console, including event properties for easy debugging */ -@@ -585,7 +564,6 @@ declare namespace posthog { - request_batching?: boolean - sanitize_properties?: (properties: posthog.Properties, event_name: string) => posthog.Properties - properties_string_max_length?: number -- session_recording?: SessionRecordingOptions - mask_all_element_attributes?: boolean - mask_all_text?: boolean - advanced_disable_decide?: boolean -@@ -618,17 +596,6 @@ declare namespace posthog { - send_event: boolean - } - -- interface SessionRecordingOptions { -- blockClass?: string | RegExp -- blockSelector?: string -- ignoreClass?: string -- maskAllInputs?: boolean -- maskInputOptions?: MaskInputOptions -- maskInputFn?: (text: string) => string -- slimDOMOptions?: SlimDOMOptions | 'all' | true -- collectFonts?: boolean -- } -- - export class persistence { - static properties(): posthog.Properties - -@@ -768,12 +735,6 @@ declare namespace posthog { - export class feature_flags extends featureFlags {} - } - --export class SentryIntegration implements Integration { -- constructor(posthog: posthog, organization?: string, projectId?: number, prefix?: string) -- name: string -- setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void --} -- - export type PostHog = typeof posthog - - export default posthog diff --git a/yarn.lock b/yarn.lock index 6bb5654db8..891822a66e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1790,11 +1790,6 @@ object.fromentries "^2.0.0" prop-types "^15.7.0" -"@yarnpkg/lockfile@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" - integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== - abab@^2.0.3, abab@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" @@ -2718,7 +2713,7 @@ cross-fetch@^3.0.4: dependencies: node-fetch "2.6.1" -cross-spawn@^6.0.0, cross-spawn@^6.0.5: +cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -3669,13 +3664,6 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -find-yarn-workspace-root@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd" - integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ== - dependencies: - micromatch "^4.0.2" - flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -3755,15 +3743,6 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" -fs-extra@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" - integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== - dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" - fs-readdir-recursive@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" @@ -3940,7 +3919,7 @@ gonzales-pe@^4.3.0: dependencies: minimist "^1.2.5" -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.4: +graceful-fs@^4.1.11, graceful-fs@^4.2.4: version "4.2.6" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== @@ -4616,7 +4595,7 @@ is-windows@^1.0.2: resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== -is-wsl@^2.1.1, is-wsl@^2.2.0: +is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== @@ -5238,13 +5217,6 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= - optionalDependencies: - graceful-fs "^4.1.6" - jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -5294,13 +5266,6 @@ kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -klaw-sync@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" - integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== - dependencies: - graceful-fs "^4.1.11" - kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -5972,14 +5937,6 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" -open@^7.4.2: - version "7.4.2" - resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" - integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== - dependencies: - is-docker "^2.0.0" - is-wsl "^2.1.1" - optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -6009,11 +5966,6 @@ opus-recorder@^8.0.3: resolved "https://registry.yarnpkg.com/opus-recorder/-/opus-recorder-8.0.3.tgz#f7b44f8f68500c9b96a15042a69f915fd9c1716d" integrity sha512-8vXGiRwlJAavT9D3yYzukNVXQ8vEcKHcsQL/zXO24DQtJ0PLXvoPHNQPJrbMCdB4ypJgWDExvHF4JitQDL7dng== -os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - p-each-series@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a" @@ -6122,25 +6074,6 @@ pascalcase@^0.1.1: resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= -patch-package@^6.4.7: - version "6.4.7" - resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-6.4.7.tgz#2282d53c397909a0d9ef92dae3fdeb558382b148" - integrity sha512-S0vh/ZEafZ17hbhgqdnpunKDfzHQibQizx9g8yEf5dcVk3KOflOfdufRXQX8CSEkyOQwuM/bNz1GwKvFj54kaQ== - dependencies: - "@yarnpkg/lockfile" "^1.1.0" - chalk "^2.4.2" - cross-spawn "^6.0.5" - find-yarn-workspace-root "^2.0.0" - fs-extra "^7.0.1" - is-ci "^2.0.0" - klaw-sync "^6.0.0" - minimist "^1.2.0" - open "^7.4.2" - rimraf "^2.6.3" - semver "^5.6.0" - slash "^2.0.0" - tmp "^0.0.33" - path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -6321,18 +6254,13 @@ postcss@^8.0.2: nanoid "^3.1.23" source-map-js "^0.6.2" -posthog-js@^1.12.2: +posthog-js@1.12.2: version "1.12.2" resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.12.2.tgz#ff76e26634067e003f8af7df654d7ea0e647d946" integrity sha512-I0d6c+Yu2f91PFidz65AIkkqZM219EY9Z1wlbTkW5Zqfq5oXqogBMKS8BaDBOrMc46LjLX7IH67ytCcBFRo1uw== dependencies: fflate "^0.4.1" -postinstall-postinstall@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" - integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ== - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -6906,13 +6834,6 @@ rfc4648@^1.4.0: resolved "https://registry.yarnpkg.com/rfc4648/-/rfc4648-1.5.0.tgz#1ba940ec1649685ec4d88788dc57fb8e18855055" integrity sha512-FA6W9lDNeX8WbMY31io1xWg+TpZCbeDKsBo0ocwACZiWnh9TUAyk9CCuBQuOPmYnwwdEQZmraQ2ZK7yJsxErBg== -rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -7599,13 +7520,6 @@ tmatch@^2.0.1: resolved "https://registry.yarnpkg.com/tmatch/-/tmatch-2.0.1.tgz#0c56246f33f30da1b8d3d72895abaf16660f38cf" integrity sha1-DFYkbzPzDaG409colauvFmYPOM8= -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" @@ -7851,7 +7765,7 @@ unist-util-stringify-position@^2.0.0: dependencies: "@types/unist" "^2.0.2" -universalify@^0.1.0, universalify@^0.1.2: +universalify@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== From 17b935cbc4d7b9ab86e2a885c5da0bd0ff5a252b Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 4 Aug 2021 09:34:12 +0100 Subject: [PATCH 111/115] Add @sentry/types and rrweb-snapshot as dev dependencies This is neccessary to resolve re-exported types referred to by posthog-js' type definitions. This isn't ideal, but * We intend to start using sentry anyway * Discussion with posthog maintainers about rrweb-snapshot at https://github.com/PostHog/posthog-js/issues/252, perhaps we can find another solution soon --- package.json | 2 ++ yarn.lock | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/package.json b/package.json index 8f5459ab99..0bcf86bff0 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ "@babel/traverse": "^7.12.12", "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", "@peculiar/webcrypto": "^1.1.4", + "@sentry/types": "^6.10.0", "@sinonjs/fake-timers": "^7.0.2", "@types/classnames": "^2.2.11", "@types/commonmark": "^0.27.4", @@ -167,6 +168,7 @@ "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", "react-test-renderer": "^17.0.2", "rimraf": "^3.0.2", + "rrweb-snapshot": "1.1.7", "stylelint": "^13.9.0", "stylelint-config-standard": "^20.0.0", "stylelint-scss": "^3.18.0", diff --git a/yarn.lock b/yarn.lock index 891822a66e..836301b002 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1352,6 +1352,11 @@ tslib "^2.2.0" webcrypto-core "^1.2.0" +"@sentry/types@^6.10.0": + version "6.10.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.10.0.tgz#6b1f44e5ed4dbc2710bead24d1b32fb08daf04e1" + integrity sha512-M7s0JFgG7/6/yNVYoPUbxzaXDhnzyIQYRRJJKRaTD77YO4MHvi4Ke8alBWqD5fer0cPIfcSkBqa9BLdqRqcMWw== + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -6841,6 +6846,11 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rrweb-snapshot@1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.1.7.tgz#92a3b47b1112a1b566c2fae2edb02fa48a6f6653" + integrity sha512-+f2kCCvIQ1hbEeCWnV7mPVPDEdWEExqwcYqMd/r1nfK52QE7qU52jefUOyTe85Vy67rZGqWnfK/B25e/OTSgYg== + rst-selector-parser@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" From 7cb21c845faa2ef835414dd647999479877c9b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 4 Aug 2021 10:33:37 +0200 Subject: [PATCH 112/115] Fix indent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/HtmlUtils.tsx | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 27556a4012..2eee5214af 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -57,10 +57,33 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i'); const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; -export const PERMITTED_URL_SCHEMES = ["bitcoin", "ftp", "geo", "http", "https", "im", "irc", - "ircs", "magnet", "mailto", "matrix", "mms", "news", - "nntp", "openpgp4fpr", "sip", "sftp", "sms", "smsto", - "ssh", "tel", "urn", "webcal", "wtai", "xmpp"]; +export const PERMITTED_URL_SCHEMES = [ + "bitcoin", + "ftp", + "geo", + "http", + "https", + "im", + "irc", + "ircs", + "magnet", + "mailto", + "matrix", + "mms", + "news", + "nntp", + "openpgp4fpr", + "sip", + "sftp", + "sms", + "smsto", + "ssh", + "tel", + "urn", + "webcal", + "wtai", + "xmpp", +]; const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/; From c9c8177f58353fc4b71aa39cb6b8d40ee74d4779 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 4 Aug 2021 09:46:39 +0100 Subject: [PATCH 113/115] Start a call immediately after creating a room via the dial pad (#6529) --- src/CallHandler.tsx | 2 ++ test/CallHandler-test.ts | 6 +++++- test/test-utils.js | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index caf010200a..30fcf46790 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -930,6 +930,8 @@ export default class CallHandler extends EventEmitter { action: 'view_room', room_id: roomId, }); + + await this.placeCall(roomId, PlaceCallType.Voice, null); } private async startTransferToPhoneNumber(call: MatrixCall, destination: string, consultFirst: boolean) { diff --git a/test/CallHandler-test.ts b/test/CallHandler-test.ts index 4c3dd25fb4..1a4560ad4a 100644 --- a/test/CallHandler-test.ts +++ b/test/CallHandler-test.ts @@ -156,13 +156,14 @@ describe('CallHandler', () => { DMRoomMap.setShared(null); // @ts-ignore window.mxCallHandler = null; + fakeCall = null; MatrixClientPeg.unset(); document.body.removeChild(audioElement); SdkConfig.unset(); }); - it('should look up the correct user and open the room when a phone number is dialled', async () => { + it('should look up the correct user and start a call in the room when a phone number is dialled', async () => { MatrixClientPeg.get().getThirdpartyUser = jest.fn().mockResolvedValue([{ userid: '@user2:example.org', protocol: "im.vector.protocol.sip_native", @@ -179,6 +180,9 @@ describe('CallHandler', () => { const viewRoomPayload = await untilDispatch('view_room'); expect(viewRoomPayload.room_id).toEqual(MAPPED_ROOM_ID); + + // Check that a call was started + expect(fakeCall.roomId).toEqual(MAPPED_ROOM_ID); }); it('should move calls between rooms when remote asserted identity changes', async () => { diff --git a/test/test-utils.js b/test/test-utils.js index 217c399443..bbab47589a 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -78,6 +78,7 @@ export function createTestClient() { }, mxcUrlToHttp: (mxc) => 'http://this.is.a.url/', setAccountData: jest.fn(), + setRoomAccountData: jest.fn(), sendTyping: jest.fn().mockResolvedValue({}), sendMessage: () => jest.fn().mockResolvedValue({}), getSyncState: () => "SYNCING", From c31f9335dc4ee7a6f5a601e0afda46d3dcc6e1d6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 4 Aug 2021 11:51:55 +0100 Subject: [PATCH 114/115] Declare node 14 requirement Mostly for netlify (we need node 14 now because element-web's webpack config uses the `??` operator). --- .node-version | 1 + 1 file changed, 1 insertion(+) create mode 100644 .node-version diff --git a/.node-version b/.node-version new file mode 100644 index 0000000000..8351c19397 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +14 From 6350cecb5e4c389ef19c17aca9569b03553bd18a Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 4 Aug 2021 12:02:14 +0100 Subject: [PATCH 115/115] emoty commit to try & wake netlify up