Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/18858
This commit is contained in:
commit
b052d0730d
24 changed files with 824 additions and 774 deletions
13
package.json
13
package.json
|
@ -93,10 +93,10 @@
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
"re-resizable": "^6.9.0",
|
"re-resizable": "^6.9.0",
|
||||||
"react": "^17.0.2",
|
"react": "17.0.2",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-blurhash": "^0.1.3",
|
"react-blurhash": "^0.1.3",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-focus-lock": "^2.5.0",
|
"react-focus-lock": "^2.5.0",
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
|
@ -142,9 +142,9 @@
|
||||||
"@types/pako": "^1.0.1",
|
"@types/pako": "^1.0.1",
|
||||||
"@types/parse5": "^6.0.0",
|
"@types/parse5": "^6.0.0",
|
||||||
"@types/qrcode": "^1.3.5",
|
"@types/qrcode": "^1.3.5",
|
||||||
"@types/react": "^17.0.2",
|
"@types/react": "17.0.14",
|
||||||
"@types/react-beautiful-dnd": "^13.0.0",
|
"@types/react-beautiful-dnd": "^13.0.0",
|
||||||
"@types/react-dom": "^17.0.2",
|
"@types/react-dom": "17.0.9",
|
||||||
"@types/react-transition-group": "^4.4.0",
|
"@types/react-transition-group": "^4.4.0",
|
||||||
"@types/sanitize-html": "^2.3.1",
|
"@types/sanitize-html": "^2.3.1",
|
||||||
"@types/zxcvbn": "^4.4.0",
|
"@types/zxcvbn": "^4.4.0",
|
||||||
|
@ -175,9 +175,12 @@
|
||||||
"stylelint": "^13.9.0",
|
"stylelint": "^13.9.0",
|
||||||
"stylelint-config-standard": "^20.0.0",
|
"stylelint-config-standard": "^20.0.0",
|
||||||
"stylelint-scss": "^3.18.0",
|
"stylelint-scss": "^3.18.0",
|
||||||
"typescript": "^4.1.3",
|
"typescript": "4.3.5",
|
||||||
"walk": "^2.3.14"
|
"walk": "^2.3.14"
|
||||||
},
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"@types/react": "17.0.14"
|
||||||
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"testEnvironment": "./__test-utils__/environment.js",
|
"testEnvironment": "./__test-utils__/environment.js",
|
||||||
"testMatch": [
|
"testMatch": [
|
||||||
|
|
|
@ -23,11 +23,11 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile[data-layout=bubble] {
|
.mx_EventTile[data-layout=bubble] {
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: var(--gutterSize);
|
margin-top: var(--gutterSize);
|
||||||
margin-left: 50px;
|
margin-left: 49px;
|
||||||
margin-right: 100px;
|
margin-right: 100px;
|
||||||
|
font-size: $font-14px;
|
||||||
|
|
||||||
&.mx_EventTile_continuation {
|
&.mx_EventTile_continuation {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
@ -77,10 +77,11 @@ limitations under the License.
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SenderProfile {
|
> .mx_SenderProfile {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -2px;
|
top: -2px;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
|
font-size: $font-15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-self=false] {
|
&[data-self=false] {
|
||||||
|
@ -113,8 +114,6 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_ReplyTile .mx_SenderProfile {
|
.mx_ReplyTile .mx_SenderProfile {
|
||||||
display: block;
|
display: block;
|
||||||
top: unset;
|
|
||||||
left: unset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ReactionsRow {
|
.mx_ReactionsRow {
|
||||||
|
@ -287,6 +286,8 @@ limitations under the License.
|
||||||
.mx_EventTile_line,
|
.mx_EventTile_line,
|
||||||
.mx_EventTile_info {
|
.mx_EventTile_info {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
// Preserve alignment with left edge of text in bubbles
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_e2eIcon {
|
.mx_EventTile_e2eIcon {
|
||||||
|
@ -294,9 +295,10 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_line > a {
|
.mx_EventTile_line > a {
|
||||||
|
// Align timestamps with those of normal bubble tiles
|
||||||
right: auto;
|
right: auto;
|
||||||
top: -15px;
|
top: -11px;
|
||||||
left: -68px;
|
left: -95px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,11 +328,10 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line {
|
||||||
margin: 0 5px;
|
margin: 0;
|
||||||
> a {
|
> a {
|
||||||
left: auto;
|
// Align timestamps with those of normal bubble tiles
|
||||||
right: 0;
|
left: -76px;
|
||||||
transform: translateX(calc(100% + 5px));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,7 +341,8 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventListSummary[data-expanded=false][data-layout=bubble] {
|
.mx_EventListSummary[data-expanded=false][data-layout=bubble] {
|
||||||
padding: 0 34px;
|
// Align with left edge of bubble tiles
|
||||||
|
padding: 0 49px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* events that do not require bubble layout */
|
/* events that do not require bubble layout */
|
||||||
|
|
|
@ -172,14 +172,12 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In the general case, we leave height of headers alone even if sticky, so
|
// In the general case, we reserve space for each sublist header to prevent
|
||||||
// that the sublists below them do not jump. However, that leaves a gap
|
// scroll jumps when they become sticky. However, that leaves a gap when
|
||||||
// when scrolled to the top above the first sublist (whose header can only
|
// scrolled to the top above the first sublist (whose header can only ever
|
||||||
// ever stick to top), so we force height to 0 for only that first header.
|
// stick to top), so we make sure to exclude the first visible sublist.
|
||||||
// See also https://github.com/vector-im/element-web/issues/14429.
|
&:not(.mx_RoomSublist_hidden) ~ .mx_RoomSublist .mx_RoomSublist_headerContainer {
|
||||||
&:first-child .mx_RoomSublist_headerContainer {
|
height: 24px;
|
||||||
height: 0;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomSublist_resizeBox {
|
.mx_RoomSublist_resizeBox {
|
||||||
|
|
|
@ -574,11 +574,12 @@ async function doSetLoggedIn(
|
||||||
await abortLogin();
|
await abortLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
PosthogAnalytics.instance.updateAnonymityFromSettings(credentials.userId);
|
|
||||||
|
|
||||||
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl);
|
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl);
|
||||||
|
|
||||||
MatrixClientPeg.replaceUsingCreds(credentials);
|
MatrixClientPeg.replaceUsingCreds(credentials);
|
||||||
|
|
||||||
|
PosthogAnalytics.instance.updateAnonymityFromSettings(credentials.userId);
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
if (credentials.freshLogin && SettingsStore.getValue("feature_dehydration")) {
|
if (credentials.freshLogin && SettingsStore.getValue("feature_dehydration")) {
|
||||||
|
|
|
@ -17,8 +17,8 @@ limitations under the License.
|
||||||
|
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import { SettingLevel } from "./settings/SettingLevel";
|
import { SettingLevel } from "./settings/SettingLevel";
|
||||||
import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix";
|
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
|
|
||||||
// XXX: MediaDeviceKind is a union type, so we make our own enum
|
// XXX: MediaDeviceKind is a union type, so we make our own enum
|
||||||
export enum MediaDeviceKindEnum {
|
export enum MediaDeviceKindEnum {
|
||||||
|
@ -74,8 +74,8 @@ export default class MediaDeviceHandler extends EventEmitter {
|
||||||
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
||||||
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
||||||
|
|
||||||
setMatrixCallAudioInput(audioDeviceId);
|
MatrixClientPeg.get().getMediaHandler().setAudioInput(audioDeviceId);
|
||||||
setMatrixCallVideoInput(videoDeviceId);
|
MatrixClientPeg.get().getMediaHandler().setVideoInput(videoDeviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setAudioOutput(deviceId: string): void {
|
public setAudioOutput(deviceId: string): void {
|
||||||
|
@ -90,7 +90,7 @@ export default class MediaDeviceHandler extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
public setAudioInput(deviceId: string): void {
|
public setAudioInput(deviceId: string): void {
|
||||||
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
||||||
setMatrixCallAudioInput(deviceId);
|
MatrixClientPeg.get().getMediaHandler().setAudioInput(deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,7 +100,7 @@ export default class MediaDeviceHandler extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
public setVideoInput(deviceId: string): void {
|
public setVideoInput(deviceId: string): void {
|
||||||
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
|
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
|
||||||
setMatrixCallVideoInput(deviceId);
|
MatrixClientPeg.get().getMediaHandler().setVideoInput(deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setDevice(deviceId: string, kind: MediaDeviceKindEnum): void {
|
public setDevice(deviceId: string, kind: MediaDeviceKindEnum): void {
|
||||||
|
|
|
@ -18,6 +18,8 @@ import posthog, { PostHog } from 'posthog-js';
|
||||||
import PlatformPeg from './PlatformPeg';
|
import PlatformPeg from './PlatformPeg';
|
||||||
import SdkConfig from './SdkConfig';
|
import SdkConfig from './SdkConfig';
|
||||||
import SettingsStore from './settings/SettingsStore';
|
import SettingsStore from './settings/SettingsStore';
|
||||||
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
/* Posthog analytics tracking.
|
/* Posthog analytics tracking.
|
||||||
*
|
*
|
||||||
|
@ -27,10 +29,11 @@ import SettingsStore from './settings/SettingsStore';
|
||||||
* - If [Do Not Track](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack) is
|
* - 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
|
* enabled, events are not sent (this detection is built into posthog and turned on via the
|
||||||
* `respect_dnt` flag being passed to `posthog.init`).
|
* `respect_dnt` flag being passed to `posthog.init`).
|
||||||
* - If the `feature_pseudonymous_analytics_opt_in` labs flag is `true`, track pseudonomously, i.e.
|
* - If the `feature_pseudonymous_analytics_opt_in` labs flag is `true`, track pseudonomously by maintaining
|
||||||
* hash all matrix identifiers in tracking events (user IDs, room IDs etc) using SHA-256.
|
* a randomised analytics ID in account_data for that user (shared between devices) and sending it to posthog to
|
||||||
* - Otherwise, if the existing `analyticsOptIn` flag is `true`, track anonymously, i.e.
|
identify the user.
|
||||||
* redact all matrix identifiers in tracking events.
|
* - Otherwise, if the existing `analyticsOptIn` flag is `true`, track anonymously, i.e. do not identify the user
|
||||||
|
using any identifier that would be consistent across devices.
|
||||||
* - If both flags are false or not set, events are not sent.
|
* - If both flags are false or not set, events are not sent.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -71,12 +74,6 @@ interface IPageView extends IAnonymousEvent {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashHex = async (input: string): Promise<string> => {
|
|
||||||
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("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const whitelistedScreens = new Set([
|
const whitelistedScreens = new Set([
|
||||||
"register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory",
|
"register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory",
|
||||||
"start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group",
|
"start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group",
|
||||||
|
@ -89,7 +86,6 @@ export async function getRedactedCurrentLocation(
|
||||||
anonymity: Anonymity,
|
anonymity: Anonymity,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// Redact PII from the current location.
|
// 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 /<screen name>/might/be/pii
|
// For known screens, assumes a URL structure of /<screen name>/might/be/pii
|
||||||
if (origin.startsWith('file://')) {
|
if (origin.startsWith('file://')) {
|
||||||
pathname = "/<redacted_file_scheme_url>/";
|
pathname = "/<redacted_file_scheme_url>/";
|
||||||
|
@ -99,17 +95,13 @@ export async function getRedactedCurrentLocation(
|
||||||
if (hash == "") {
|
if (hash == "") {
|
||||||
hashStr = "";
|
hashStr = "";
|
||||||
} else {
|
} else {
|
||||||
let [beforeFirstSlash, screen, ...parts] = hash.split("/");
|
let [beforeFirstSlash, screen] = hash.split("/");
|
||||||
|
|
||||||
if (!whitelistedScreens.has(screen)) {
|
if (!whitelistedScreens.has(screen)) {
|
||||||
screen = "<redacted_screen_name>";
|
screen = "<redacted_screen_name>";
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < parts.length; i++) {
|
hashStr = `${beforeFirstSlash}/${screen}/<redacted>`;
|
||||||
parts[i] = anonymity === Anonymity.Anonymous ? `<redacted>` : await hashHex(parts[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
hashStr = `${beforeFirstSlash}/${screen}/${parts.join("/")}`;
|
|
||||||
}
|
}
|
||||||
return origin + pathname + hashStr;
|
return origin + pathname + hashStr;
|
||||||
}
|
}
|
||||||
|
@ -123,15 +115,15 @@ export class PosthogAnalytics {
|
||||||
/* Wrapper for Posthog analytics.
|
/* Wrapper for Posthog analytics.
|
||||||
* 3 modes of anonymity are supported, governed by this.anonymity
|
* 3 modes of anonymity are supported, governed by this.anonymity
|
||||||
* - Anonymity.Disabled means *no data* is passed to posthog
|
* - Anonymity.Disabled means *no data* is passed to posthog
|
||||||
* - Anonymity.Anonymous means all identifers will be redacted before being passed to posthog
|
* - Anonymity.Anonymous means no identifier is passed to posthog
|
||||||
* - Anonymity.Pseudonymous means all identifiers will be hashed via SHA-256 before being passed
|
* - Anonymity.Pseudonymous means an analytics ID stored in account_data and shared between devices
|
||||||
* to Posthog
|
* is passed to posthog.
|
||||||
*
|
*
|
||||||
* To update anonymity, call updateAnonymityFromSettings() or you can set it directly via setAnonymity().
|
* To update anonymity, call updateAnonymityFromSettings() or you can set it directly via setAnonymity().
|
||||||
*
|
*
|
||||||
* To pass an event to Posthog:
|
* To pass an event to Posthog:
|
||||||
*
|
*
|
||||||
* 1. Declare a type for the event, extending IAnonymousEvent, IPseudonymousEvent or IRoomEvent.
|
* 1. Declare a type for the event, extending IAnonymousEvent or IPseudonymousEvent.
|
||||||
* 2. Call the appropriate track*() method. Pseudonymous events will be dropped when anonymity is
|
* 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.
|
* Anonymous or Disabled; Anonymous events will be dropped when anonymity is Disabled.
|
||||||
*/
|
*/
|
||||||
|
@ -141,6 +133,7 @@ export class PosthogAnalytics {
|
||||||
private enabled = false;
|
private enabled = false;
|
||||||
private static _instance = null;
|
private static _instance = null;
|
||||||
private platformSuperProperties = {};
|
private platformSuperProperties = {};
|
||||||
|
private static ANALYTICS_ID_EVENT_TYPE = "im.vector.web.analytics_id";
|
||||||
|
|
||||||
public static get instance(): PosthogAnalytics {
|
public static get instance(): PosthogAnalytics {
|
||||||
if (!this._instance) {
|
if (!this._instance) {
|
||||||
|
@ -274,9 +267,32 @@ export class PosthogAnalytics {
|
||||||
this.anonymity = anonymity;
|
this.anonymity = anonymity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async identifyUser(userId: string): Promise<void> {
|
private static getRandomAnalyticsId(): string {
|
||||||
|
return [...crypto.getRandomValues(new Uint8Array(16))].map((c) => c.toString(16)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async identifyUser(client: MatrixClient, analyticsIdGenerator: () => string): Promise<void> {
|
||||||
if (this.anonymity == Anonymity.Pseudonymous) {
|
if (this.anonymity == Anonymity.Pseudonymous) {
|
||||||
this.posthog.identify(await hashHex(userId));
|
// Check the user's account_data for an analytics ID to use. Storing the ID in account_data allows
|
||||||
|
// different devices to send the same ID.
|
||||||
|
try {
|
||||||
|
const accountData = await client.getAccountDataFromServer(PosthogAnalytics.ANALYTICS_ID_EVENT_TYPE);
|
||||||
|
let analyticsID = accountData?.id;
|
||||||
|
if (!analyticsID) {
|
||||||
|
// Couldn't retrieve an analytics ID from user settings, so create one and set it on the server.
|
||||||
|
// Note there's a race condition here - if two devices do these steps at the same time, last write
|
||||||
|
// wins, and the first writer will send tracking with an ID that doesn't match the one on the server
|
||||||
|
// until the next time account data is refreshed and this function is called (most likely on next
|
||||||
|
// page load). This will happen pretty infrequently, so we can tolerate the possibility.
|
||||||
|
analyticsID = analyticsIdGenerator();
|
||||||
|
await client.setAccountData("im.vector.web.analytics_id", { id: analyticsID });
|
||||||
|
}
|
||||||
|
this.posthog.identify(analyticsID);
|
||||||
|
} catch (e) {
|
||||||
|
// The above could fail due to network requests, but not essential to starting the application,
|
||||||
|
// so swallow it.
|
||||||
|
console.log("Unable to identify user for tracking" + e.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -307,18 +323,6 @@ export class PosthogAnalytics {
|
||||||
await this.capture(eventName, properties);
|
await this.capture(eventName, properties);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async trackRoomEvent<E extends IRoomEvent>(
|
|
||||||
eventName: E["eventName"],
|
|
||||||
roomId: string,
|
|
||||||
properties: Omit<E["properties"], "roomId">,
|
|
||||||
): Promise<void> {
|
|
||||||
const updatedProperties = {
|
|
||||||
...properties,
|
|
||||||
hashedRoomId: roomId ? await hashHex(roomId) : null,
|
|
||||||
};
|
|
||||||
await this.trackPseudonymousEvent(eventName, updatedProperties);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async trackPageView(durationMs: number): Promise<void> {
|
public async trackPageView(durationMs: number): Promise<void> {
|
||||||
const hash = window.location.hash;
|
const hash = window.location.hash;
|
||||||
|
|
||||||
|
@ -349,7 +353,7 @@ export class PosthogAnalytics {
|
||||||
// Identify the user (via hashed user ID) to posthog if anonymity is pseudonmyous
|
// Identify the user (via hashed user ID) to posthog if anonymity is pseudonmyous
|
||||||
this.setAnonymity(PosthogAnalytics.getAnonymityFromSettings());
|
this.setAnonymity(PosthogAnalytics.getAnonymityFromSettings());
|
||||||
if (userId && this.getAnonymity() == Anonymity.Pseudonymous) {
|
if (userId && this.getAnonymity() == Anonymity.Pseudonymous) {
|
||||||
await this.identifyUser(userId);
|
await this.identifyUser(MatrixClientPeg.get(), PosthogAnalytics.getRandomAnalyticsId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,11 +48,6 @@ export default class Resend {
|
||||||
// XXX: temporary logging to try to diagnose
|
// XXX: temporary logging to try to diagnose
|
||||||
// https://github.com/vector-im/element-web/issues/3148
|
// https://github.com/vector-im/element-web/issues/3148
|
||||||
console.log('Resend got send failure: ' + err.name + '(' + err + ')');
|
console.log('Resend got send failure: ' + err.name + '(' + err + ')');
|
||||||
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'message_send_failed',
|
|
||||||
event: event,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1897,15 +1897,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
onSendEvent(roomId: string, event: MatrixEvent) {
|
onSendEvent(roomId: string, event: MatrixEvent) {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (!cli) {
|
if (!cli) return;
|
||||||
dis.dispatch({ action: 'message_send_failed' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cli.sendEvent(roomId, event.getType(), event.getContent()).then(() => {
|
cli.sendEvent(roomId, event.getType(), event.getContent()).then(() => {
|
||||||
dis.dispatch({ action: 'message_sent' });
|
dis.dispatch({ action: 'message_sent' });
|
||||||
}, (err) => {
|
|
||||||
dis.dispatch({ action: 'message_send_failed' });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,43 +15,48 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import MemberAvatar from '../avatars/MemberAvatar';
|
import MemberAvatar from '../avatars/MemberAvatar';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
|
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
import { ChevronFace, ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
member: RoomMember;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
resizeMethod?: ResizeMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
hasStatus: boolean;
|
||||||
|
menuDisplayed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.avatars.MemberStatusMessageAvatar")
|
@replaceableComponent("views.avatars.MemberStatusMessageAvatar")
|
||||||
export default class MemberStatusMessageAvatar extends React.Component {
|
export default class MemberStatusMessageAvatar extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
public static defaultProps: Partial<IProps> = {
|
||||||
member: PropTypes.object.isRequired,
|
|
||||||
width: PropTypes.number,
|
|
||||||
height: PropTypes.number,
|
|
||||||
resizeMethod: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
resizeMethod: 'crop',
|
resizeMethod: 'crop',
|
||||||
};
|
};
|
||||||
|
private button = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
hasStatus: this.hasStatus,
|
hasStatus: this.hasStatus,
|
||||||
menuDisplayed: false,
|
menuDisplayed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._button = createRef();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
|
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
|
||||||
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
|
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
|
||||||
}
|
}
|
||||||
|
@ -62,44 +67,44 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
|
user.on("User._unstable_statusMessage", this.onStatusMessageCommitted);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
const { user } = this.props.member;
|
const { user } = this.props.member;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
user.removeListener(
|
user.removeListener(
|
||||||
"User._unstable_statusMessage",
|
"User._unstable_statusMessage",
|
||||||
this._onStatusMessageCommitted,
|
this.onStatusMessageCommitted,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasStatus() {
|
private get hasStatus(): boolean {
|
||||||
const { user } = this.props.member;
|
const { user } = this.props.member;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !!user._unstable_statusMessage;
|
return !!user.unstable_statusMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onStatusMessageCommitted = () => {
|
private onStatusMessageCommitted = (): void => {
|
||||||
// The `User` object has observed a status message change.
|
// The `User` object has observed a status message change.
|
||||||
this.setState({
|
this.setState({
|
||||||
hasStatus: this.hasStatus,
|
hasStatus: this.hasStatus,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
openMenu = () => {
|
private openMenu = (): void => {
|
||||||
this.setState({ menuDisplayed: true });
|
this.setState({ menuDisplayed: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
closeMenu = () => {
|
private closeMenu = (): void => {
|
||||||
this.setState({ menuDisplayed: false });
|
this.setState({ menuDisplayed: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const avatar = <MemberAvatar
|
const avatar = <MemberAvatar
|
||||||
member={this.props.member}
|
member={this.props.member}
|
||||||
width={this.props.width}
|
width={this.props.width}
|
||||||
|
@ -118,7 +123,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||||
|
|
||||||
let contextMenu;
|
let contextMenu;
|
||||||
if (this.state.menuDisplayed) {
|
if (this.state.menuDisplayed) {
|
||||||
const elementRect = this._button.current.getBoundingClientRect();
|
const elementRect = this.button.current.getBoundingClientRect();
|
||||||
|
|
||||||
const chevronWidth = 16; // See .mx_ContextualMenu_chevron_bottom
|
const chevronWidth = 16; // See .mx_ContextualMenu_chevron_bottom
|
||||||
const chevronMargin = 1; // Add some spacing away from target
|
const chevronMargin = 1; // Add some spacing away from target
|
||||||
|
@ -126,13 +131,13 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
chevronOffset={(elementRect.width - chevronWidth) / 2}
|
chevronOffset={(elementRect.width - chevronWidth) / 2}
|
||||||
chevronFace="bottom"
|
chevronFace={ChevronFace.Bottom}
|
||||||
left={elementRect.left + window.pageXOffset}
|
left={elementRect.left + window.pageXOffset}
|
||||||
top={elementRect.top + window.pageYOffset - chevronMargin}
|
top={elementRect.top + window.pageYOffset - chevronMargin}
|
||||||
menuWidth={226}
|
menuWidth={226}
|
||||||
onFinished={this.closeMenu}
|
onFinished={this.closeMenu}
|
||||||
>
|
>
|
||||||
<StatusMessageContextMenu user={this.props.member.user} onFinished={this.closeMenu} />
|
<StatusMessageContextMenu user={this.props.member.user} />
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -140,7 +145,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
<ContextMenuButton
|
<ContextMenuButton
|
||||||
className={classes}
|
className={classes}
|
||||||
inputRef={this._button}
|
inputRef={this.button}
|
||||||
onClick={this.openMenu}
|
onClick={this.openMenu}
|
||||||
isExpanded={this.state.menuDisplayed}
|
isExpanded={this.state.menuDisplayed}
|
||||||
label={_t("User Status")}
|
label={_t("User Status")}
|
|
@ -15,45 +15,41 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
/*
|
interface IProps {
|
||||||
|
element: React.ReactNode;
|
||||||
|
// Function to be called when the parent window is resized
|
||||||
|
// This can be used to reposition or close the menu on resize and
|
||||||
|
// ensure that it is not displayed in a stale position.
|
||||||
|
onResize?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
* This component can be used to display generic HTML content in a contextual
|
* This component can be used to display generic HTML content in a contextual
|
||||||
* menu.
|
* menu.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@replaceableComponent("views.context_menus.GenericElementContextMenu")
|
@replaceableComponent("views.context_menus.GenericElementContextMenu")
|
||||||
export default class GenericElementContextMenu extends React.Component {
|
export default class GenericElementContextMenu extends React.Component<IProps> {
|
||||||
static propTypes = {
|
constructor(props: IProps) {
|
||||||
element: PropTypes.element.isRequired,
|
|
||||||
// Function to be called when the parent window is resized
|
|
||||||
// This can be used to reposition or close the menu on resize and
|
|
||||||
// ensure that it is not displayed in a stale position.
|
|
||||||
onResize: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
super(props);
|
||||||
this.resize = this.resize.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this.resize = this.resize.bind(this);
|
|
||||||
window.addEventListener("resize", this.resize);
|
window.addEventListener("resize", this.resize);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
window.removeEventListener("resize", this.resize);
|
window.removeEventListener("resize", this.resize);
|
||||||
}
|
}
|
||||||
|
|
||||||
resize() {
|
private resize = (): void => {
|
||||||
if (this.props.onResize) {
|
if (this.props.onResize) {
|
||||||
this.props.onResize();
|
this.props.onResize();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
return <div>{ this.props.element }</div>;
|
return <div>{ this.props.element }</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -15,16 +15,15 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.context_menus.GenericTextContextMenu")
|
interface IProps {
|
||||||
export default class GenericTextContextMenu extends React.Component {
|
message: string;
|
||||||
static propTypes = {
|
}
|
||||||
message: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
@replaceableComponent("views.context_menus.GenericTextContextMenu")
|
||||||
|
export default class GenericTextContextMenu extends React.Component<IProps> {
|
||||||
|
public render(): JSX.Element {
|
||||||
return <div>{ this.props.message }</div>;
|
return <div>{ this.props.message }</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -14,53 +14,59 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { ChangeEvent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import * as sdk from '../../../index';
|
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
|
import Spinner from "../elements/Spinner";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// js-sdk User object. Not required because it might not exist.
|
||||||
|
user?: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
message: string;
|
||||||
|
waiting: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.context_menus.StatusMessageContextMenu")
|
@replaceableComponent("views.context_menus.StatusMessageContextMenu")
|
||||||
export default class StatusMessageContextMenu extends React.Component {
|
export default class StatusMessageContextMenu extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
constructor(props: IProps) {
|
||||||
// js-sdk User object. Not required because it might not exist.
|
|
||||||
user: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
message: this.comittedStatusMessage,
|
message: this.comittedStatusMessage,
|
||||||
|
waiting: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
|
user.on("User._unstable_statusMessage", this.onStatusMessageCommitted);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
user.removeListener(
|
user.removeListener(
|
||||||
"User._unstable_statusMessage",
|
"User._unstable_statusMessage",
|
||||||
this._onStatusMessageCommitted,
|
this.onStatusMessageCommitted,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get comittedStatusMessage() {
|
get comittedStatusMessage(): string {
|
||||||
return this.props.user ? this.props.user._unstable_statusMessage : "";
|
return this.props.user ? this.props.user.unstable_statusMessage : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
_onStatusMessageCommitted = () => {
|
private onStatusMessageCommitted = (): void => {
|
||||||
// The `User` object has observed a status message change.
|
// The `User` object has observed a status message change.
|
||||||
this.setState({
|
this.setState({
|
||||||
message: this.comittedStatusMessage,
|
message: this.comittedStatusMessage,
|
||||||
|
@ -68,14 +74,14 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onClearClick = (e) => {
|
private onClearClick = (): void=> {
|
||||||
MatrixClientPeg.get()._unstable_setStatusMessage("");
|
MatrixClientPeg.get()._unstable_setStatusMessage("");
|
||||||
this.setState({
|
this.setState({
|
||||||
waiting: true,
|
waiting: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onSubmit = (e) => {
|
private onSubmit = (e: ButtonEvent): void => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message);
|
MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message);
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -83,27 +89,25 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onStatusChange = (e) => {
|
private onStatusChange = (e: ChangeEvent): void => {
|
||||||
// The input field's value was changed.
|
// The input field's value was changed.
|
||||||
this.setState({
|
this.setState({
|
||||||
message: e.target.value,
|
message: (e.target as HTMLInputElement).value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
|
||||||
|
|
||||||
let actionButton;
|
let actionButton;
|
||||||
if (this.comittedStatusMessage) {
|
if (this.comittedStatusMessage) {
|
||||||
if (this.state.message === this.comittedStatusMessage) {
|
if (this.state.message === this.comittedStatusMessage) {
|
||||||
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_clear"
|
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_clear"
|
||||||
onClick={this._onClearClick}
|
onClick={this.onClearClick}
|
||||||
>
|
>
|
||||||
<span>{ _t("Clear status") }</span>
|
<span>{ _t("Clear status") }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
} else {
|
} else {
|
||||||
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
|
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
|
||||||
onClick={this._onSubmit}
|
onClick={this.onSubmit}
|
||||||
>
|
>
|
||||||
<span>{ _t("Update status") }</span>
|
<span>{ _t("Update status") }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
|
@ -112,7 +116,7 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
actionButton = <AccessibleButton
|
actionButton = <AccessibleButton
|
||||||
className="mx_StatusMessageContextMenu_submit"
|
className="mx_StatusMessageContextMenu_submit"
|
||||||
disabled={!this.state.message}
|
disabled={!this.state.message}
|
||||||
onClick={this._onSubmit}
|
onClick={this.onSubmit}
|
||||||
>
|
>
|
||||||
<span>{ _t("Set status") }</span>
|
<span>{ _t("Set status") }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
|
@ -120,13 +124,13 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
|
|
||||||
let spinner = null;
|
let spinner = null;
|
||||||
if (this.state.waiting) {
|
if (this.state.waiting) {
|
||||||
spinner = <Spinner w="24" h="24" />;
|
spinner = <Spinner w={24} h={24} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = <form
|
const form = <form
|
||||||
className="mx_StatusMessageContextMenu_form"
|
className="mx_StatusMessageContextMenu_form"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onSubmit={this._onSubmit}
|
onSubmit={this.onSubmit}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -134,9 +138,9 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||||
key="message"
|
key="message"
|
||||||
placeholder={_t("Set a new status...")}
|
placeholder={_t("Set a new status...")}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
maxLength="60"
|
maxLength={60}
|
||||||
value={this.state.message}
|
value={this.state.message}
|
||||||
onChange={this._onStatusChange}
|
onChange={this.onStatusChange}
|
||||||
/>
|
/>
|
||||||
<div className="mx_StatusMessageContextMenu_actionContainer">
|
<div className="mx_StatusMessageContextMenu_actionContainer">
|
||||||
{ actionButton }
|
{ actionButton }
|
|
@ -429,7 +429,7 @@ const UserOptionsSection: React.FC<{
|
||||||
if (!isMe) {
|
if (!isMe) {
|
||||||
directMessageButton = (
|
directMessageButton = (
|
||||||
<AccessibleButton onClick={() => { openDMForUser(cli, member.userId); }} className="mx_UserInfo_field">
|
<AccessibleButton onClick={() => { openDMForUser(cli, member.userId); }} className="mx_UserInfo_field">
|
||||||
{ _t('Direct message') }
|
{ _t("Message") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,8 @@ import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
// matches emoticons which follow the start of a line or whitespace
|
// matches emoticons which follow the start of a line or whitespace
|
||||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s|:^$');
|
||||||
|
export const REGEX_EMOTICON = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')$');
|
||||||
|
|
||||||
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
|
||||||
|
|
||||||
|
@ -161,7 +162,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private replaceEmoticon = (caretPosition: DocumentPosition): number => {
|
public replaceEmoticon(caretPosition: DocumentPosition, regex: RegExp): number {
|
||||||
const { model } = this.props;
|
const { model } = this.props;
|
||||||
const range = model.startRange(caretPosition);
|
const range = model.startRange(caretPosition);
|
||||||
// expand range max 8 characters backwards from caretPosition,
|
// expand range max 8 characters backwards from caretPosition,
|
||||||
|
@ -170,9 +171,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
range.expandBackwardsWhile((index, offset) => {
|
range.expandBackwardsWhile((index, offset) => {
|
||||||
const part = model.parts[index];
|
const part = model.parts[index];
|
||||||
n -= 1;
|
n -= 1;
|
||||||
return n >= 0 && (part.type === Type.Plain || part.type === Type.PillCandidate);
|
return n >= 0 && [Type.Plain, Type.PillCandidate, Type.Newline].includes(part.type);
|
||||||
});
|
});
|
||||||
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text);
|
const emoticonMatch = regex.exec(range.text);
|
||||||
if (emoticonMatch) {
|
if (emoticonMatch) {
|
||||||
const query = emoticonMatch[1].replace("-", "");
|
const query = emoticonMatch[1].replace("-", "");
|
||||||
// try both exact match and lower-case, this means that xd won't match xD but :P will match :p
|
// try both exact match and lower-case, this means that xd won't match xD but :P will match :p
|
||||||
|
@ -180,18 +181,23 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
const { partCreator } = model;
|
const { partCreator } = model;
|
||||||
const hasPrecedingSpace = emoticonMatch[0][0] === " ";
|
const moveStart = emoticonMatch[0][0] === " " ? 1 : 0;
|
||||||
|
const moveEnd = emoticonMatch[0].length - emoticonMatch.length - moveStart;
|
||||||
|
|
||||||
// we need the range to only comprise of the emoticon
|
// we need the range to only comprise of the emoticon
|
||||||
// because we'll replace the whole range with an emoji,
|
// because we'll replace the whole range with an emoji,
|
||||||
// so move the start forward to the start of the emoticon.
|
// so move the start forward to the start of the emoticon.
|
||||||
// Take + 1 because index is reported without the possible preceding space.
|
// Take + 1 because index is reported without the possible preceding space.
|
||||||
range.moveStart(emoticonMatch.index + (hasPrecedingSpace ? 1 : 0));
|
range.moveStartForwards(emoticonMatch.index + moveStart);
|
||||||
|
// and move end backwards so that we don't replace the trailing space/newline
|
||||||
|
range.moveEndBackwards(moveEnd);
|
||||||
|
|
||||||
// this returns the amount of added/removed characters during the replace
|
// this returns the amount of added/removed characters during the replace
|
||||||
// so the caret position can be adjusted.
|
// so the caret position can be adjusted.
|
||||||
return range.replace([partCreator.plain(data.unicode + " ")]);
|
return range.replace([partCreator.plain(data.unicode)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => {
|
private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => {
|
||||||
renderModel(this.editorRef.current, this.props.model);
|
renderModel(this.editorRef.current, this.props.model);
|
||||||
|
@ -607,8 +613,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
};
|
};
|
||||||
|
|
||||||
private configureEmoticonAutoReplace = (): void => {
|
private configureEmoticonAutoReplace = (): void => {
|
||||||
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
|
this.props.model.setTransformCallback(this.transform);
|
||||||
this.props.model.setTransformCallback(shouldReplace ? this.replaceEmoticon : null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private configureShouldShowPillAvatar = (): void => {
|
private configureShouldShowPillAvatar = (): void => {
|
||||||
|
@ -621,6 +626,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
this.setState({ surroundWith });
|
this.setState({ surroundWith });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private transform = (documentPosition: DocumentPosition): void => {
|
||||||
|
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
|
||||||
|
if (shouldReplace) this.replaceEmoticon(documentPosition, REGEX_EMOTICON_WHITESPACE);
|
||||||
|
};
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener("selectionchange", this.onSelectionChange);
|
document.removeEventListener("selectionchange", this.onSelectionChange);
|
||||||
this.editorRef.current.removeEventListener("input", this.onInput, true);
|
this.editorRef.current.removeEventListener("input", this.onInput, true);
|
||||||
|
|
|
@ -1192,14 +1192,19 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
const thread = ReplyThread.makeThread(
|
let thread;
|
||||||
this.props.mxEvent,
|
// When the "showHiddenEventsInTimeline" lab is enabled,
|
||||||
this.props.onHeightChanged,
|
// avoid showing replies for hidden events (events without tiles)
|
||||||
this.props.permalinkCreator,
|
if (haveTileForEvent(this.props.mxEvent)) {
|
||||||
this.replyThread,
|
thread = ReplyThread.makeThread(
|
||||||
this.props.layout,
|
this.props.mxEvent,
|
||||||
this.props.alwaysShowTimestamps || this.state.hover,
|
this.props.onHeightChanged,
|
||||||
);
|
this.props.permalinkCreator,
|
||||||
|
this.replyThread,
|
||||||
|
this.props.layout,
|
||||||
|
this.props.alwaysShowTimestamps || this.state.hover,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
|
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@ let instanceCount = 0;
|
||||||
const NARROW_MODE_BREAKPOINT = 500;
|
const NARROW_MODE_BREAKPOINT = 500;
|
||||||
|
|
||||||
interface IComposerAvatarProps {
|
interface IComposerAvatarProps {
|
||||||
me: object;
|
me: RoomMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComposerAvatar(props: IComposerAvatarProps) {
|
function ComposerAvatar(props: IComposerAvatarProps) {
|
||||||
|
|
|
@ -547,7 +547,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
const unfilteredHistorical = unfilteredLists[DefaultTagID.Archived] || [];
|
const unfilteredHistorical = unfilteredLists[DefaultTagID.Archived] || [];
|
||||||
const unfilteredFavourite = unfilteredLists[DefaultTagID.Favourite] || [];
|
const unfilteredFavourite = unfilteredLists[DefaultTagID.Favourite] || [];
|
||||||
// show a prompt to join/create rooms if the user is in 0 rooms and no historical
|
// show a prompt to join/create rooms if the user is in 0 rooms and no historical
|
||||||
if (unfilteredRooms.length < 1 && unfilteredHistorical < 1 && unfilteredFavourite < 1) {
|
if (unfilteredRooms.length < 1 && unfilteredHistorical.length < 1 && unfilteredFavourite.length < 1) {
|
||||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||||
<div>{ _t("Use the + to make a new room or explore existing ones below") }</div>
|
<div>{ _t("Use the + to make a new room or explore existing ones below") }</div>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
|
|
|
@ -31,8 +31,8 @@ import {
|
||||||
textSerialize,
|
textSerialize,
|
||||||
unescapeMessage,
|
unescapeMessage,
|
||||||
} from '../../../editor/serialize';
|
} from '../../../editor/serialize';
|
||||||
|
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
||||||
import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from '../../../editor/parts';
|
import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from '../../../editor/parts';
|
||||||
import BasicMessageComposer from "./BasicMessageComposer";
|
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
import { findEditableEvent } from '../../../utils/EventUtils';
|
import { findEditableEvent } from '../../../utils/EventUtils';
|
||||||
import SendHistoryManager from "../../../SendHistoryManager";
|
import SendHistoryManager from "../../../SendHistoryManager";
|
||||||
|
@ -347,15 +347,24 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendMessage(): Promise<void> {
|
public async sendMessage(): Promise<void> {
|
||||||
if (this.model.isEmpty) {
|
const model = this.model;
|
||||||
|
|
||||||
|
if (model.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace emoticon at the end of the message
|
||||||
|
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
||||||
|
const caret = this.editorRef.current?.getCaret();
|
||||||
|
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||||
|
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
||||||
|
}
|
||||||
|
|
||||||
const replyToEvent = this.props.replyToEvent;
|
const replyToEvent = this.props.replyToEvent;
|
||||||
let shouldSend = true;
|
let shouldSend = true;
|
||||||
let content;
|
let content;
|
||||||
|
|
||||||
if (!containsEmote(this.model) && this.isSlashCommand()) {
|
if (!containsEmote(model) && this.isSlashCommand()) {
|
||||||
const [cmd, args, commandText] = this.getSlashCommand();
|
const [cmd, args, commandText] = this.getSlashCommand();
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
if (cmd.category === CommandCategories.messages) {
|
if (cmd.category === CommandCategories.messages) {
|
||||||
|
@ -400,7 +409,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isQuickReaction(this.model)) {
|
if (isQuickReaction(model)) {
|
||||||
shouldSend = false;
|
shouldSend = false;
|
||||||
this.sendQuickReaction();
|
this.sendQuickReaction();
|
||||||
}
|
}
|
||||||
|
@ -410,7 +419,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
const { roomId } = this.props.room;
|
const { roomId } = this.props.room;
|
||||||
if (!content) {
|
if (!content) {
|
||||||
content = createMessageContent(
|
content = createMessageContent(
|
||||||
this.model,
|
model,
|
||||||
replyToEvent,
|
replyToEvent,
|
||||||
this.props.replyInThread,
|
this.props.replyInThread,
|
||||||
this.props.permalinkCreator,
|
this.props.permalinkCreator,
|
||||||
|
@ -446,9 +455,9 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
|
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendHistoryManager.save(this.model, replyToEvent);
|
this.sendHistoryManager.save(model, replyToEvent);
|
||||||
// clear composer
|
// clear composer
|
||||||
this.model.reset([]);
|
model.reset([]);
|
||||||
this.editorRef.current?.clearUndoHistory();
|
this.editorRef.current?.clearUndoHistory();
|
||||||
this.editorRef.current?.focus();
|
this.editorRef.current?.focus();
|
||||||
this.clearStoredEditorState();
|
this.clearStoredEditorState();
|
||||||
|
|
|
@ -277,9 +277,13 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
if (this.state.screensharing) {
|
if (this.state.screensharing) {
|
||||||
isScreensharing = await this.props.call.setScreensharingEnabled(false);
|
isScreensharing = await this.props.call.setScreensharingEnabled(false);
|
||||||
} else {
|
} else {
|
||||||
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
|
if (window.electron?.getDesktopCapturerSources) {
|
||||||
const [source] = await finished;
|
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
|
||||||
isScreensharing = await this.props.call.setScreensharingEnabled(true, source);
|
const [source] = await finished;
|
||||||
|
isScreensharing = await this.props.call.setScreensharingEnabled(true, source);
|
||||||
|
} else {
|
||||||
|
isScreensharing = await this.props.call.setScreensharingEnabled(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|
|
@ -32,13 +32,20 @@ export default class Range {
|
||||||
this._end = bIsLarger ? positionB : positionA;
|
this._end = bIsLarger ? positionB : positionA;
|
||||||
}
|
}
|
||||||
|
|
||||||
public moveStart(delta: number): void {
|
public moveStartForwards(delta: number): void {
|
||||||
this._start = this._start.forwardsWhile(this.model, () => {
|
this._start = this._start.forwardsWhile(this.model, () => {
|
||||||
delta -= 1;
|
delta -= 1;
|
||||||
return delta >= 0;
|
return delta >= 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public moveEndBackwards(delta: number): void {
|
||||||
|
this._end = this._end.backwardsWhile(this.model, () => {
|
||||||
|
delta -= 1;
|
||||||
|
return delta >= 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public trim(): void {
|
public trim(): void {
|
||||||
this._start = this._start.forwardsWhile(this.model, whitespacePredicate);
|
this._start = this._start.forwardsWhile(this.model, whitespacePredicate);
|
||||||
this._end = this._end.backwardsWhile(this.model, whitespacePredicate);
|
this._end = this._end.backwardsWhile(this.model, whitespacePredicate);
|
||||||
|
|
|
@ -1842,7 +1842,7 @@
|
||||||
"Mention": "Mention",
|
"Mention": "Mention",
|
||||||
"Invite": "Invite",
|
"Invite": "Invite",
|
||||||
"Share Link to User": "Share Link to User",
|
"Share Link to User": "Share Link to User",
|
||||||
"Direct message": "Direct message",
|
"Message": "Message",
|
||||||
"Demote yourself?": "Demote yourself?",
|
"Demote yourself?": "Demote yourself?",
|
||||||
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.",
|
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.",
|
||||||
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.",
|
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.",
|
||||||
|
|
|
@ -852,10 +852,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Action.SwitchSpace:
|
case Action.SwitchSpace:
|
||||||
if (payload.num === 0) {
|
// 1 is Home, 2-9 are the spaces after Home
|
||||||
|
if (payload.num === 1) {
|
||||||
this.setActiveSpace(null);
|
this.setActiveSpace(null);
|
||||||
} else if (this.spacePanelSpaces.length >= payload.num) {
|
} else if (this.spacePanelSpaces.length >= payload.num) {
|
||||||
this.setActiveSpace(this.spacePanelSpaces[payload.num - 1]);
|
this.setActiveSpace(this.spacePanelSpaces[payload.num - 2]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
|
@ -136,18 +136,6 @@ describe("PosthogAnalytics", () => {
|
||||||
expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar");
|
expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should pass trackRoomEvent to posthog", async () => {
|
|
||||||
analytics.setAnonymity(Anonymity.Pseudonymous);
|
|
||||||
const roomId = "42";
|
|
||||||
await analytics.trackRoomEvent<IRoomEvent>("jest_test_event", roomId, {
|
|
||||||
foo: "bar",
|
|
||||||
});
|
|
||||||
expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event");
|
|
||||||
expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar");
|
|
||||||
expect(fakePosthog.capture.mock.calls[0][1]["hashedRoomId"])
|
|
||||||
.toEqual("73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should pass trackPseudonymousEvent() to posthog", async () => {
|
it("Should pass trackPseudonymousEvent() to posthog", async () => {
|
||||||
analytics.setAnonymity(Anonymity.Pseudonymous);
|
analytics.setAnonymity(Anonymity.Pseudonymous);
|
||||||
await analytics.trackPseudonymousEvent<ITestEvent>("jest_test_pseudo_event", {
|
await analytics.trackPseudonymousEvent<ITestEvent>("jest_test_pseudo_event", {
|
||||||
|
@ -173,9 +161,6 @@ describe("PosthogAnalytics", () => {
|
||||||
await analytics.trackAnonymousEvent<ITestEvent>("jest_test_event", {
|
await analytics.trackAnonymousEvent<ITestEvent>("jest_test_event", {
|
||||||
foo: "bar",
|
foo: "bar",
|
||||||
});
|
});
|
||||||
await analytics.trackRoomEvent<ITestRoomEvent>("room id", "foo", {
|
|
||||||
foo: "bar",
|
|
||||||
});
|
|
||||||
await analytics.trackPageView(200);
|
await analytics.trackPageView(200);
|
||||||
expect(fakePosthog.capture.mock.calls.length).toBe(0);
|
expect(fakePosthog.capture.mock.calls.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
@ -183,31 +168,25 @@ describe("PosthogAnalytics", () => {
|
||||||
it("Should pseudonymise a location of a known screen", async () => {
|
it("Should pseudonymise a location of a known screen", async () => {
|
||||||
const location = await getRedactedCurrentLocation(
|
const location = await getRedactedCurrentLocation(
|
||||||
"https://foo.bar", "#/register/some/pii", "/", Anonymity.Pseudonymous);
|
"https://foo.bar", "#/register/some/pii", "/", Anonymity.Pseudonymous);
|
||||||
expect(location).toBe(
|
expect(location).toBe("https://foo.bar/#/register/<redacted>");
|
||||||
`https://foo.bar/#/register/\
|
|
||||||
a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b/\
|
|
||||||
bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should anonymise a location of a known screen", async () => {
|
it("Should anonymise a location of a known screen", async () => {
|
||||||
const location = await getRedactedCurrentLocation(
|
const location = await getRedactedCurrentLocation(
|
||||||
"https://foo.bar", "#/register/some/pii", "/", Anonymity.Anonymous);
|
"https://foo.bar", "#/register/some/pii", "/", Anonymity.Anonymous);
|
||||||
expect(location).toBe("https://foo.bar/#/register/<redacted>/<redacted>");
|
expect(location).toBe("https://foo.bar/#/register/<redacted>");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should pseudonymise a location of an unknown screen", async () => {
|
it("Should pseudonymise a location of an unknown screen", async () => {
|
||||||
const location = await getRedactedCurrentLocation(
|
const location = await getRedactedCurrentLocation(
|
||||||
"https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Pseudonymous);
|
"https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Pseudonymous);
|
||||||
expect(location).toBe(
|
expect(location).toBe("https://foo.bar/#/<redacted_screen_name>/<redacted>");
|
||||||
`https://foo.bar/#/<redacted_screen_name>/\
|
|
||||||
a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b/\
|
|
||||||
bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should anonymise a location of an unknown screen", async () => {
|
it("Should anonymise a location of an unknown screen", async () => {
|
||||||
const location = await getRedactedCurrentLocation(
|
const location = await getRedactedCurrentLocation(
|
||||||
"https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Anonymous);
|
"https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Anonymous);
|
||||||
expect(location).toBe("https://foo.bar/#/<redacted_screen_name>/<redacted>/<redacted>");
|
expect(location).toBe("https://foo.bar/#/<redacted_screen_name>/<redacted>");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should handle an empty hash", async () => {
|
it("Should handle an empty hash", async () => {
|
||||||
|
@ -218,15 +197,28 @@ bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`);
|
||||||
|
|
||||||
it("Should identify the user to posthog if pseudonymous", async () => {
|
it("Should identify the user to posthog if pseudonymous", async () => {
|
||||||
analytics.setAnonymity(Anonymity.Pseudonymous);
|
analytics.setAnonymity(Anonymity.Pseudonymous);
|
||||||
await analytics.identifyUser("foo");
|
class FakeClient {
|
||||||
expect(fakePosthog.identify.mock.calls[0][0])
|
getAccountDataFromServer = jest.fn().mockResolvedValue(null);
|
||||||
.toBe("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae");
|
setAccountData = jest.fn().mockResolvedValue({});
|
||||||
|
}
|
||||||
|
await analytics.identifyUser(new FakeClient(), () => "analytics_id" );
|
||||||
|
expect(fakePosthog.identify.mock.calls[0][0]).toBe("analytics_id");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should not identify the user to posthog if anonymous", async () => {
|
it("Should not identify the user to posthog if anonymous", async () => {
|
||||||
analytics.setAnonymity(Anonymity.Anonymous);
|
analytics.setAnonymity(Anonymity.Anonymous);
|
||||||
await analytics.identifyUser("foo");
|
await analytics.identifyUser(null);
|
||||||
expect(fakePosthog.identify.mock.calls.length).toBe(0);
|
expect(fakePosthog.identify.mock.calls.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should identify using the server's analytics id if present", async () => {
|
||||||
|
analytics.setAnonymity(Anonymity.Pseudonymous);
|
||||||
|
class FakeClient {
|
||||||
|
getAccountDataFromServer = jest.fn().mockResolvedValue({ id: "existing_analytics_id" });
|
||||||
|
setAccountData = jest.fn().mockResolvedValue({});
|
||||||
|
}
|
||||||
|
await analytics.identifyUser(new FakeClient(), () => "new_analytics_id" );
|
||||||
|
expect(fakePosthog.identify.mock.calls[0][0]).toBe("existing_analytics_id");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue