Merge remote-tracking branch 'upstream/develop' into task/dialogs-ts
This commit is contained in:
commit
31e1831f02
71 changed files with 1742 additions and 1476 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": [
|
||||||
|
|
|
@ -73,12 +73,6 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation
|
||||||
.mx_GroupFilterPanel .mx_TagTile {
|
.mx_GroupFilterPanel .mx_TagTile {
|
||||||
// opacity: 0.5;
|
// opacity: 0.5;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.mx_BetaDot {
|
|
||||||
position: absolute;
|
|
||||||
right: -13px;
|
|
||||||
top: -11px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype {
|
.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype {
|
||||||
|
|
|
@ -103,6 +103,16 @@ $activeBorderColor: $secondary-content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SpaceItem_new {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.mx_BetaDot {
|
||||||
|
position: absolute;
|
||||||
|
left: 33px;
|
||||||
|
top: -5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton {
|
.mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton {
|
||||||
margin-left: $gutterSize;
|
margin-left: $gutterSize;
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
|
@ -194,23 +204,18 @@ $activeBorderColor: $secondary-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_SpaceButton_new .mx_SpaceButton_icon {
|
&.mx_SpaceButton_new .mx_SpaceButton_icon {
|
||||||
background-color: $accent-color;
|
background-color: $roomlist-button-bg-color;
|
||||||
transition: all .1s ease-in-out; // TODO transition
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
background-color: #ffffff;
|
background-color: $primary-content;
|
||||||
mask-image: url('$(res)/img/element-icons/plus.svg');
|
mask-image: url('$(res)/img/element-icons/plus.svg');
|
||||||
transition: all .2s ease-in-out; // TODO transition
|
transition: all .2s ease-in-out; // TODO transition
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_SpaceButton_newCancel .mx_SpaceButton_icon {
|
&.mx_SpaceButton_newCancel .mx_SpaceButton_icon::before {
|
||||||
background-color: $icon-button-color;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_BaseAvatar_image {
|
.mx_BaseAvatar_image {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
|
@ -215,10 +215,11 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .mx_BaseAvatar_image,
|
> .mx_RoomAvatar_isSpaceRoom {
|
||||||
> .mx_BaseAvatar > .mx_BaseAvatar_image {
|
&.mx_BaseAvatar_image, .mx_BaseAvatar_image {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
h1.mx_SpaceRoomView_preview_name {
|
h1.mx_SpaceRoomView_preview_name {
|
||||||
margin: 20px 0 !important; // override default margin from above
|
margin: 20px 0 !important; // override default margin from above
|
||||||
|
|
|
@ -113,6 +113,7 @@ $dot-size: 12px;
|
||||||
animation: mx_Beta_bluePulse 2s infinite;
|
animation: mx_Beta_bluePulse 2s infinite;
|
||||||
animation-iteration-count: 20;
|
animation-iteration-count: 20;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -21,6 +21,17 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_SettingsTab_section {
|
.mx_SettingsTab_section {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
> details {
|
||||||
|
> summary {
|
||||||
|
cursor: pointer;
|
||||||
|
color: $primary-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + .mx_SettingsFlag {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_PreferencesUserSettingsTab_CommunityMigrator {
|
.mx_PreferencesUserSettingsTab_CommunityMigrator {
|
||||||
|
|
0
res/img/betas/.gitkeep
Normal file
0
res/img/betas/.gitkeep
Normal file
Binary file not shown.
Before Width: | Height: | Size: 380 KiB |
|
@ -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,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import request from 'browser-request';
|
import request from 'browser-request';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import sanitizeHtml from 'sanitize-html';
|
import sanitizeHtml from 'sanitize-html';
|
||||||
|
@ -26,38 +25,43 @@ import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
|
|
||||||
export default class EmbeddedPage extends React.PureComponent {
|
interface IProps {
|
||||||
static propTypes = {
|
|
||||||
// URL to request embedded page content from
|
// URL to request embedded page content from
|
||||||
url: PropTypes.string,
|
url?: string;
|
||||||
// Class name prefix to apply for a given instance
|
// Class name prefix to apply for a given instance
|
||||||
className: PropTypes.string,
|
className?: string;
|
||||||
// Whether to wrap the page in a scrollbar
|
// Whether to wrap the page in a scrollbar
|
||||||
scrollbar: PropTypes.bool,
|
scrollbar?: boolean;
|
||||||
// Map of keys to replace with values, e.g {$placeholder: "value"}
|
// Map of keys to replace with values, e.g {$placeholder: "value"}
|
||||||
replaceMap: PropTypes.object,
|
replaceMap?: Map<string, string>;
|
||||||
};
|
}
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
interface IState {
|
||||||
|
page: string;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props, context) {
|
export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
|
||||||
|
public static contextType = MatrixClientContext;
|
||||||
|
private unmounted = false;
|
||||||
|
private dispatcherRef: string = null;
|
||||||
|
|
||||||
|
constructor(props: IProps, context: typeof MatrixClientContext) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this._dispatcherRef = null;
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
page: '',
|
page: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
translate(s) {
|
protected translate(s: string): string {
|
||||||
// default implementation - skins may wish to extend this
|
// default implementation - skins may wish to extend this
|
||||||
return sanitizeHtml(_t(s));
|
return sanitizeHtml(_t(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this._unmounted = false;
|
this.unmounted = false;
|
||||||
|
|
||||||
if (!this.props.url) {
|
if (!this.props.url) {
|
||||||
return;
|
return;
|
||||||
|
@ -70,7 +74,7 @@ export default class EmbeddedPage extends React.PureComponent {
|
||||||
request(
|
request(
|
||||||
{ method: "GET", url: this.props.url },
|
{ method: "GET", url: this.props.url },
|
||||||
(err, response, body) => {
|
(err, response, body) => {
|
||||||
if (this._unmounted) {
|
if (this.unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,22 +96,22 @@ export default class EmbeddedPage extends React.PureComponent {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
this._dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
this._unmounted = true;
|
this.unmounted = true;
|
||||||
if (this._dispatcherRef !== null) dis.unregister(this._dispatcherRef);
|
if (this.dispatcherRef !== null) dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
onAction = (payload) => {
|
private onAction = (payload: ActionPayload): void => {
|
||||||
// HACK: Workaround for the context's MatrixClient not being set up at render time.
|
// HACK: Workaround for the context's MatrixClient not being set up at render time.
|
||||||
if (payload.action === 'client_started') {
|
if (payload.action === 'client_started') {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
// HACK: Workaround for the context's MatrixClient not updating.
|
// HACK: Workaround for the context's MatrixClient not updating.
|
||||||
const client = this.context || MatrixClientPeg.get();
|
const client = this.context || MatrixClientPeg.get();
|
||||||
const isGuest = client ? client.isGuest() : true;
|
const isGuest = client ? client.isGuest() : true;
|
|
@ -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("structures.GenericErrorPage")
|
interface IProps {
|
||||||
export default class GenericErrorPage extends React.PureComponent {
|
title: React.ReactNode;
|
||||||
static propTypes = {
|
message: React.ReactNode;
|
||||||
title: PropTypes.object.isRequired, // jsx for title
|
}
|
||||||
message: PropTypes.object.isRequired, // jsx to display
|
|
||||||
};
|
|
||||||
|
|
||||||
|
@replaceableComponent("structures.GenericErrorPage")
|
||||||
|
export default class GenericErrorPage extends React.PureComponent<IProps> {
|
||||||
render() {
|
render() {
|
||||||
return <div className='mx_GenericErrorPage'>
|
return <div className='mx_GenericErrorPage'>
|
||||||
<div className='mx_GenericErrorPage_box'>
|
<div className='mx_GenericErrorPage_box'>
|
|
@ -146,19 +146,13 @@ class GroupFilterPanel extends React.Component<IGroupFilterPanelProps, IGroupFil
|
||||||
mx_GroupFilterPanel_items_selected: itemsSelected,
|
mx_GroupFilterPanel_items_selected: itemsSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
let betaDot;
|
|
||||||
if (SettingsStore.getBetaInfo("feature_spaces") && !localStorage.getItem("mx_seenSpacesBeta")) {
|
|
||||||
betaDot = <div className="mx_BetaDot" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let createButton = (
|
let createButton = (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
tooltip
|
tooltip
|
||||||
label={_t("Communities")}
|
label={_t("Communities")}
|
||||||
action="toggle_my_groups"
|
action="toggle_my_groups"
|
||||||
className="mx_TagTile mx_TagTile_plus">
|
className="mx_TagTile mx_TagTile_plus"
|
||||||
{ betaDot }
|
/>
|
||||||
</ActionButton>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
|
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
|
||||||
|
|
|
@ -14,34 +14,39 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { createRef } from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("structures.IndicatorScrollbar")
|
interface IProps {
|
||||||
export default class IndicatorScrollbar extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
// If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator
|
// If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator
|
||||||
// and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning
|
// and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning
|
||||||
// by the parent element.
|
// by the parent element.
|
||||||
trackHorizontalOverflow: PropTypes.bool,
|
trackHorizontalOverflow?: boolean;
|
||||||
|
|
||||||
// If true, when the user tries to use their mouse wheel in the component it will
|
// If true, when the user tries to use their mouse wheel in the component it will
|
||||||
// scroll horizontally rather than vertically. This should only be used on components
|
// scroll horizontally rather than vertically. This should only be used on components
|
||||||
// with no vertical scroll opportunity.
|
// with no vertical scroll opportunity.
|
||||||
verticalScrollsHorizontally: PropTypes.bool,
|
verticalScrollsHorizontally?: boolean;
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
children: React.ReactNode;
|
||||||
|
className: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
leftIndicatorOffset: number | string;
|
||||||
|
rightIndicatorOffset: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("structures.IndicatorScrollbar")
|
||||||
|
export default class IndicatorScrollbar extends React.Component<IProps, IState> {
|
||||||
|
private autoHideScrollbar = createRef<AutoHideScrollbar>();
|
||||||
|
private scrollElement: HTMLDivElement;
|
||||||
|
private likelyTrackpadUser: boolean = null;
|
||||||
|
private checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this._collectScroller = this._collectScroller.bind(this);
|
|
||||||
this._collectScrollerComponent = this._collectScrollerComponent.bind(this);
|
|
||||||
this.checkOverflow = this.checkOverflow.bind(this);
|
|
||||||
this._scrollElement = null;
|
|
||||||
this._autoHideScrollbar = null;
|
|
||||||
this._likelyTrackpadUser = null;
|
|
||||||
this._checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
leftIndicatorOffset: 0,
|
leftIndicatorOffset: 0,
|
||||||
|
@ -49,30 +54,19 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
moveToOrigin() {
|
private collectScroller = (scroller: HTMLDivElement): void => {
|
||||||
if (!this._scrollElement) return;
|
if (scroller && !this.scrollElement) {
|
||||||
|
this.scrollElement = scroller;
|
||||||
this._scrollElement.scrollLeft = 0;
|
|
||||||
this._scrollElement.scrollTop = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_collectScroller(scroller) {
|
|
||||||
if (scroller && !this._scrollElement) {
|
|
||||||
this._scrollElement = scroller;
|
|
||||||
// Using the passive option to not block the main thread
|
// Using the passive option to not block the main thread
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||||
this._scrollElement.addEventListener("scroll", this.checkOverflow, { passive: true });
|
this.scrollElement.addEventListener("scroll", this.checkOverflow, { passive: true });
|
||||||
this.checkOverflow();
|
this.checkOverflow();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_collectScrollerComponent(autoHideScrollbar) {
|
public componentDidUpdate(prevProps: IProps): void {
|
||||||
this._autoHideScrollbar = autoHideScrollbar;
|
const prevLen = React.Children.count(prevProps.children);
|
||||||
}
|
const curLen = React.Children.count(this.props.children);
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const prevLen = prevProps && prevProps.children && prevProps.children.length || 0;
|
|
||||||
const curLen = this.props.children && this.props.children.length || 0;
|
|
||||||
// check overflow only if amount of children changes.
|
// check overflow only if amount of children changes.
|
||||||
// if we don't guard here, we end up with an infinite
|
// if we don't guard here, we end up with an infinite
|
||||||
// render > componentDidUpdate > checkOverflow > setState > render loop
|
// render > componentDidUpdate > checkOverflow > setState > render loop
|
||||||
|
@ -81,62 +75,58 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this.checkOverflow();
|
this.checkOverflow();
|
||||||
}
|
}
|
||||||
|
|
||||||
checkOverflow() {
|
private checkOverflow = (): void => {
|
||||||
const hasTopOverflow = this._scrollElement.scrollTop > 0;
|
const hasTopOverflow = this.scrollElement.scrollTop > 0;
|
||||||
const hasBottomOverflow = this._scrollElement.scrollHeight >
|
const hasBottomOverflow = this.scrollElement.scrollHeight >
|
||||||
(this._scrollElement.scrollTop + this._scrollElement.clientHeight);
|
(this.scrollElement.scrollTop + this.scrollElement.clientHeight);
|
||||||
const hasLeftOverflow = this._scrollElement.scrollLeft > 0;
|
const hasLeftOverflow = this.scrollElement.scrollLeft > 0;
|
||||||
const hasRightOverflow = this._scrollElement.scrollWidth >
|
const hasRightOverflow = this.scrollElement.scrollWidth >
|
||||||
(this._scrollElement.scrollLeft + this._scrollElement.clientWidth);
|
(this.scrollElement.scrollLeft + this.scrollElement.clientWidth);
|
||||||
|
|
||||||
if (hasTopOverflow) {
|
if (hasTopOverflow) {
|
||||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow");
|
this.scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow");
|
||||||
} else {
|
} else {
|
||||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_topOverflow");
|
this.scrollElement.classList.remove("mx_IndicatorScrollbar_topOverflow");
|
||||||
}
|
}
|
||||||
if (hasBottomOverflow) {
|
if (hasBottomOverflow) {
|
||||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_bottomOverflow");
|
this.scrollElement.classList.add("mx_IndicatorScrollbar_bottomOverflow");
|
||||||
} else {
|
} else {
|
||||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
|
this.scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
|
||||||
}
|
}
|
||||||
if (hasLeftOverflow) {
|
if (hasLeftOverflow) {
|
||||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_leftOverflow");
|
this.scrollElement.classList.add("mx_IndicatorScrollbar_leftOverflow");
|
||||||
} else {
|
} else {
|
||||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_leftOverflow");
|
this.scrollElement.classList.remove("mx_IndicatorScrollbar_leftOverflow");
|
||||||
}
|
}
|
||||||
if (hasRightOverflow) {
|
if (hasRightOverflow) {
|
||||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_rightOverflow");
|
this.scrollElement.classList.add("mx_IndicatorScrollbar_rightOverflow");
|
||||||
} else {
|
} else {
|
||||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_rightOverflow");
|
this.scrollElement.classList.remove("mx_IndicatorScrollbar_rightOverflow");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.trackHorizontalOverflow) {
|
if (this.props.trackHorizontalOverflow) {
|
||||||
this.setState({
|
this.setState({
|
||||||
// Offset from absolute position of the container
|
// Offset from absolute position of the container
|
||||||
leftIndicatorOffset: hasLeftOverflow ? `${this._scrollElement.scrollLeft}px` : '0',
|
leftIndicatorOffset: hasLeftOverflow ? `${this.scrollElement.scrollLeft}px` : '0',
|
||||||
|
|
||||||
// Negative because we're coming from the right
|
// Negative because we're coming from the right
|
||||||
rightIndicatorOffset: hasRightOverflow ? `-${this._scrollElement.scrollLeft}px` : '0',
|
rightIndicatorOffset: hasRightOverflow ? `-${this.scrollElement.scrollLeft}px` : '0',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
getScrollTop() {
|
public componentWillUnmount(): void {
|
||||||
return this._autoHideScrollbar.getScrollTop();
|
if (this.scrollElement) {
|
||||||
}
|
this.scrollElement.removeEventListener("scroll", this.checkOverflow);
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this._scrollElement) {
|
|
||||||
this._scrollElement.removeEventListener("scroll", this.checkOverflow);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseWheel = (e) => {
|
private onMouseWheel = (e: React.WheelEvent): void => {
|
||||||
if (this.props.verticalScrollsHorizontally && this._scrollElement) {
|
if (this.props.verticalScrollsHorizontally && this.scrollElement) {
|
||||||
// xyThreshold is the amount of horizontal motion required for the component to
|
// xyThreshold is the amount of horizontal motion required for the component to
|
||||||
// ignore the vertical delta in a scroll. Used to stop trackpads from acting in
|
// ignore the vertical delta in a scroll. Used to stop trackpads from acting in
|
||||||
// strange ways. Should be positive.
|
// strange ways. Should be positive.
|
||||||
|
@ -150,19 +140,19 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
// for at least the next 1 minute.
|
// for at least the next 1 minute.
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
if (Math.abs(e.deltaX) > 0) {
|
if (Math.abs(e.deltaX) > 0) {
|
||||||
this._likelyTrackpadUser = true;
|
this.likelyTrackpadUser = true;
|
||||||
this._checkAgainForTrackpad = now + (1 * 60 * 1000);
|
this.checkAgainForTrackpad = now + (1 * 60 * 1000);
|
||||||
} else {
|
} else {
|
||||||
// if we haven't seen any horizontal scrolling for a while, assume
|
// if we haven't seen any horizontal scrolling for a while, assume
|
||||||
// the user might have plugged in a mousewheel
|
// the user might have plugged in a mousewheel
|
||||||
if (this._likelyTrackpadUser && now >= this._checkAgainForTrackpad) {
|
if (this.likelyTrackpadUser && now >= this.checkAgainForTrackpad) {
|
||||||
this._likelyTrackpadUser = false;
|
this.likelyTrackpadUser = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't mess with the horizontal scroll for trackpad users
|
// don't mess with the horizontal scroll for trackpad users
|
||||||
// See https://github.com/vector-im/element-web/issues/10005
|
// See https://github.com/vector-im/element-web/issues/10005
|
||||||
if (this._likelyTrackpadUser) {
|
if (this.likelyTrackpadUser) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,13 +168,13 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
|
|
||||||
// noinspection JSSuspiciousNameCombination
|
// noinspection JSSuspiciousNameCombination
|
||||||
const val = Math.abs(e.deltaY) < 25 ? (e.deltaY + additionalScroll) : e.deltaY;
|
const val = Math.abs(e.deltaY) < 25 ? (e.deltaY + additionalScroll) : e.deltaY;
|
||||||
this._scrollElement.scrollLeft += val * yRetention;
|
this.scrollElement.scrollLeft += val * yRetention;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props;
|
const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props;
|
||||||
|
|
||||||
const leftIndicatorStyle = { left: this.state.leftIndicatorOffset };
|
const leftIndicatorStyle = { left: this.state.leftIndicatorOffset };
|
||||||
|
@ -195,8 +185,8 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null;
|
? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null;
|
||||||
|
|
||||||
return (<AutoHideScrollbar
|
return (<AutoHideScrollbar
|
||||||
ref={this._collectScrollerComponent}
|
ref={this.autoHideScrollbar}
|
||||||
wrappedRef={this._collectScroller}
|
wrappedRef={this.collectScroller}
|
||||||
onWheel={this.onMouseWheel}
|
onWheel={this.onMouseWheel}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
115
src/components/structures/LegacyCommunityPreview.tsx
Normal file
115
src/components/structures/LegacyCommunityPreview.tsx
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
/*
|
||||||
|
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 React, { useContext } from "react";
|
||||||
|
|
||||||
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
import { _t } from "../../languageHandler";
|
||||||
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
|
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||||
|
import { IGroupSummary } from "../views/dialogs/CreateSpaceFromCommunityDialog";
|
||||||
|
import { useAsyncMemo } from "../../hooks/useAsyncMemo";
|
||||||
|
import Spinner from "../views/elements/Spinner";
|
||||||
|
import GroupAvatar from "../views/avatars/GroupAvatar";
|
||||||
|
import { linkifyElement } from "../../HtmlUtils";
|
||||||
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
|
import { Action } from "../../dispatcher/actions";
|
||||||
|
import { UserTab } from "../views/dialogs/UserSettingsDialog";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
groupId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSwapClick = () => {
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: Action.ViewUserSettings,
|
||||||
|
initialTabId: UserTab.Preferences,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// XXX: temporary community migration component, reuses SpaceRoomView & SpacePreview classes for simplicity
|
||||||
|
const LegacyCommunityPreview = ({ groupId }: IProps) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
|
const groupSummary = useAsyncMemo<IGroupSummary>(() => cli.getGroupSummary(groupId), [cli, groupId]);
|
||||||
|
|
||||||
|
if (!groupSummary) {
|
||||||
|
return <main className="mx_SpaceRoomView">
|
||||||
|
<div className="mx_MainSplit">
|
||||||
|
<div className="mx_SpaceRoomView_preview">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let visibilitySection: JSX.Element;
|
||||||
|
if (groupSummary.profile.is_public) {
|
||||||
|
visibilitySection = <span className="mx_SpaceRoomView_info_public">
|
||||||
|
{ _t("Public community") }
|
||||||
|
</span>;
|
||||||
|
} else {
|
||||||
|
visibilitySection = <span className="mx_SpaceRoomView_info_private">
|
||||||
|
{ _t("Private community") }
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <main className="mx_SpaceRoomView">
|
||||||
|
<ErrorBoundary>
|
||||||
|
<div className="mx_MainSplit">
|
||||||
|
<div className="mx_SpaceRoomView_preview">
|
||||||
|
<GroupAvatar
|
||||||
|
groupId={groupId}
|
||||||
|
groupName={groupSummary.profile.name}
|
||||||
|
groupAvatarUrl={groupSummary.profile.avatar_url}
|
||||||
|
height={80}
|
||||||
|
width={80}
|
||||||
|
resizeMethod='crop'
|
||||||
|
/>
|
||||||
|
<h1 className="mx_SpaceRoomView_preview_name">
|
||||||
|
{ groupSummary.profile.name }
|
||||||
|
</h1>
|
||||||
|
<div className="mx_SpaceRoomView_info">
|
||||||
|
{ visibilitySection }
|
||||||
|
</div>
|
||||||
|
<div className="mx_SpaceRoomView_preview_topic" ref={e => e && linkifyElement(e)}>
|
||||||
|
{ groupSummary.profile.short_description }
|
||||||
|
</div>
|
||||||
|
<div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
|
||||||
|
{ groupSummary.user?.membership === "join"
|
||||||
|
? _t("To view %(communityName)s, swap to communities in your <a>preferences</a>", {
|
||||||
|
communityName: groupSummary.profile.name,
|
||||||
|
}, {
|
||||||
|
a: sub => (
|
||||||
|
<AccessibleButton onClick={onSwapClick} kind="link">{ sub }</AccessibleButton>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
: _t("To join %(communityName)s, swap to communities in your <a>preferences</a>", {
|
||||||
|
communityName: groupSummary.profile.name,
|
||||||
|
}, {
|
||||||
|
a: sub => (
|
||||||
|
<AccessibleButton onClick={onSwapClick} kind="link">{ sub }</AccessibleButton>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</main>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LegacyCommunityPreview;
|
|
@ -69,6 +69,7 @@ import classNames from 'classnames';
|
||||||
import GroupFilterPanel from './GroupFilterPanel';
|
import GroupFilterPanel from './GroupFilterPanel';
|
||||||
import CustomRoomTagPanel from './CustomRoomTagPanel';
|
import CustomRoomTagPanel from './CustomRoomTagPanel';
|
||||||
import { mediaFromMxc } from "../../customisations/Media";
|
import { mediaFromMxc } from "../../customisations/Media";
|
||||||
|
import LegacyCommunityPreview from "./LegacyCommunityPreview";
|
||||||
|
|
||||||
// We need to fetch each pinned message individually (if we don't already have it)
|
// We need to fetch each pinned message individually (if we don't already have it)
|
||||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||||
|
@ -629,11 +630,15 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
pageElement = <UserView userId={this.props.currentUserId} resizeNotifier={this.props.resizeNotifier} />;
|
pageElement = <UserView userId={this.props.currentUserId} resizeNotifier={this.props.resizeNotifier} />;
|
||||||
break;
|
break;
|
||||||
case PageTypes.GroupView:
|
case PageTypes.GroupView:
|
||||||
|
if (SpaceStore.spacesEnabled) {
|
||||||
|
pageElement = <LegacyCommunityPreview groupId={this.props.currentGroupId} />;
|
||||||
|
} else {
|
||||||
pageElement = <GroupView
|
pageElement = <GroupView
|
||||||
groupId={this.props.currentGroupId}
|
groupId={this.props.currentGroupId}
|
||||||
isNew={this.props.currentGroupIsNew}
|
isNew={this.props.currentGroupIsNew}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
/>;
|
/>;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,25 +16,35 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Resizable } from 're-resizable';
|
import { NumberSize, Resizable } from 're-resizable';
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
|
import { Direction } from "re-resizable/lib/resizer";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
resizeNotifier: ResizeNotifier;
|
||||||
|
collapsedRhs?: boolean;
|
||||||
|
panel?: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.MainSplit")
|
@replaceableComponent("structures.MainSplit")
|
||||||
export default class MainSplit extends React.Component {
|
export default class MainSplit extends React.Component<IProps> {
|
||||||
_onResizeStart = () => {
|
private onResizeStart = (): void => {
|
||||||
this.props.resizeNotifier.startResizing();
|
this.props.resizeNotifier.startResizing();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onResize = () => {
|
private onResize = (): void => {
|
||||||
this.props.resizeNotifier.notifyRightHandleResized();
|
this.props.resizeNotifier.notifyRightHandleResized();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onResizeStop = (event, direction, refToElement, delta) => {
|
private onResizeStop = (
|
||||||
|
event: MouseEvent | TouchEvent, direction: Direction, elementRef: HTMLElement, delta: NumberSize,
|
||||||
|
): void => {
|
||||||
this.props.resizeNotifier.stopResizing();
|
this.props.resizeNotifier.stopResizing();
|
||||||
window.localStorage.setItem("mx_rhs_size", this._loadSidePanelSize().width + delta.width);
|
window.localStorage.setItem("mx_rhs_size", (this.loadSidePanelSize().width + delta.width).toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
_loadSidePanelSize() {
|
private loadSidePanelSize(): {height: string | number, width: number} {
|
||||||
let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10);
|
let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10);
|
||||||
|
|
||||||
if (isNaN(rhsSize)) {
|
if (isNaN(rhsSize)) {
|
||||||
|
@ -47,7 +57,7 @@ export default class MainSplit extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const bodyView = React.Children.only(this.props.children);
|
const bodyView = React.Children.only(this.props.children);
|
||||||
const panelView = this.props.panel;
|
const panelView = this.props.panel;
|
||||||
|
|
||||||
|
@ -56,7 +66,7 @@ export default class MainSplit extends React.Component {
|
||||||
let children;
|
let children;
|
||||||
if (hasResizer) {
|
if (hasResizer) {
|
||||||
children = <Resizable
|
children = <Resizable
|
||||||
defaultSize={this._loadSidePanelSize()}
|
defaultSize={this.loadSidePanelSize()}
|
||||||
minWidth={264}
|
minWidth={264}
|
||||||
maxWidth="50%"
|
maxWidth="50%"
|
||||||
enable={{
|
enable={{
|
||||||
|
@ -69,9 +79,9 @@ export default class MainSplit extends React.Component {
|
||||||
bottomLeft: false,
|
bottomLeft: false,
|
||||||
topLeft: false,
|
topLeft: false,
|
||||||
}}
|
}}
|
||||||
onResizeStart={this._onResizeStart}
|
onResizeStart={this.onResizeStart}
|
||||||
onResize={this._onResize}
|
onResize={this.onResize}
|
||||||
onResizeStop={this._onResizeStop}
|
onResizeStop={this.onResizeStop}
|
||||||
className="mx_RightPanel_ResizeWrapper"
|
className="mx_RightPanel_ResizeWrapper"
|
||||||
handleClasses={{ left: "mx_RightPanel_ResizeHandle" }}
|
handleClasses={{ left: "mx_RightPanel_ResizeHandle" }}
|
||||||
>
|
>
|
|
@ -1800,11 +1800,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
subAction: params.action,
|
subAction: params.action,
|
||||||
});
|
});
|
||||||
} else if (screen.indexOf('group/') === 0) {
|
} else if (screen.indexOf('group/') === 0) {
|
||||||
if (SpaceStore.spacesEnabled) {
|
|
||||||
dis.dispatch({ action: "view_home_page" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupId = screen.substring(6);
|
const groupId = screen.substring(6);
|
||||||
|
|
||||||
// TODO: Check valid group ID
|
// TODO: Check valid group ID
|
||||||
|
@ -1897,15 +1892,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' });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,6 @@ import AccessibleButton from '../views/elements/AccessibleButton';
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import BetaCard from "../views/beta/BetaCard";
|
|
||||||
|
|
||||||
@replaceableComponent("structures.MyGroups")
|
@replaceableComponent("structures.MyGroups")
|
||||||
export default class MyGroups extends React.Component {
|
export default class MyGroups extends React.Component {
|
||||||
|
@ -138,7 +137,6 @@ export default class MyGroups extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
</div>*/ }
|
</div>*/ }
|
||||||
</div>
|
</div>
|
||||||
<BetaCard featureId="feature_spaces" title={_t("Communities are changing to Spaces")} />
|
|
||||||
<div className="mx_MyGroups_content">
|
<div className="mx_MyGroups_content">
|
||||||
{ contentHeader }
|
{ contentHeader }
|
||||||
{ content }
|
{ content }
|
||||||
|
|
|
@ -15,95 +15,110 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t, _td } from '../../languageHandler';
|
import { _t, _td } from '../../languageHandler';
|
||||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
|
||||||
import Resend from '../../Resend';
|
import Resend from '../../Resend';
|
||||||
import dis from '../../dispatcher/dispatcher';
|
import dis from '../../dispatcher/dispatcher';
|
||||||
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
|
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import { EventStatus } from "matrix-js-sdk/src/models/event";
|
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import NotificationBadge from "../views/rooms/NotificationBadge";
|
import NotificationBadge from "../views/rooms/NotificationBadge";
|
||||||
import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
|
import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||||
|
import { SyncState } from "matrix-js-sdk/src/sync.api";
|
||||||
|
import { ISyncStateData } from "matrix-js-sdk/src/sync";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
const STATUS_BAR_HIDDEN = 0;
|
const STATUS_BAR_HIDDEN = 0;
|
||||||
const STATUS_BAR_EXPANDED = 1;
|
const STATUS_BAR_EXPANDED = 1;
|
||||||
const STATUS_BAR_EXPANDED_LARGE = 2;
|
const STATUS_BAR_EXPANDED_LARGE = 2;
|
||||||
|
|
||||||
export function getUnsentMessages(room) {
|
export function getUnsentMessages(room: Room): MatrixEvent[] {
|
||||||
if (!room) { return []; }
|
if (!room) { return []; }
|
||||||
return room.getPendingEvents().filter(function(ev) {
|
return room.getPendingEvents().filter(function(ev) {
|
||||||
return ev.status === EventStatus.NOT_SENT;
|
return ev.status === EventStatus.NOT_SENT;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.RoomStatusBar")
|
interface IProps {
|
||||||
export default class RoomStatusBar extends React.PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
// the room this statusbar is representing.
|
// the room this statusbar is representing.
|
||||||
room: PropTypes.object.isRequired,
|
room: Room;
|
||||||
|
|
||||||
// true if the room is being peeked at. This affects components that shouldn't
|
// true if the room is being peeked at. This affects components that shouldn't
|
||||||
// logically be shown when peeking, such as a prompt to invite people to a room.
|
// logically be shown when peeking, such as a prompt to invite people to a room.
|
||||||
isPeeking: PropTypes.bool,
|
isPeeking?: boolean;
|
||||||
|
|
||||||
// callback for when the user clicks on the 'resend all' button in the
|
// callback for when the user clicks on the 'resend all' button in the
|
||||||
// 'unsent messages' bar
|
// 'unsent messages' bar
|
||||||
onResendAllClick: PropTypes.func,
|
onResendAllClick?: () => void;
|
||||||
|
|
||||||
// callback for when the user clicks on the 'cancel all' button in the
|
// callback for when the user clicks on the 'cancel all' button in the
|
||||||
// 'unsent messages' bar
|
// 'unsent messages' bar
|
||||||
onCancelAllClick: PropTypes.func,
|
onCancelAllClick?: () => void;
|
||||||
|
|
||||||
// callback for when the user clicks on the 'invite others' button in the
|
// callback for when the user clicks on the 'invite others' button in the
|
||||||
// 'you are alone' bar
|
// 'you are alone' bar
|
||||||
onInviteClick: PropTypes.func,
|
onInviteClick?: () => void;
|
||||||
|
|
||||||
// callback for when we do something that changes the size of the
|
// callback for when we do something that changes the size of the
|
||||||
// status bar. This is used to trigger a re-layout in the parent
|
// status bar. This is used to trigger a re-layout in the parent
|
||||||
// component.
|
// component.
|
||||||
onResize: PropTypes.func,
|
onResize?: () => void;
|
||||||
|
|
||||||
// callback for when the status bar can be hidden from view, as it is
|
// callback for when the status bar can be hidden from view, as it is
|
||||||
// not displaying anything
|
// not displaying anything
|
||||||
onHidden: PropTypes.func,
|
onHidden?: () => void;
|
||||||
|
|
||||||
// callback for when the status bar is displaying something and should
|
// callback for when the status bar is displaying something and should
|
||||||
// be visible
|
// be visible
|
||||||
onVisible: PropTypes.func,
|
onVisible?: () => void;
|
||||||
};
|
}
|
||||||
|
|
||||||
state = {
|
interface IState {
|
||||||
syncState: MatrixClientPeg.get().getSyncState(),
|
syncState: SyncState;
|
||||||
syncStateData: MatrixClientPeg.get().getSyncStateData(),
|
syncStateData: ISyncStateData;
|
||||||
|
unsentMessages: MatrixEvent[];
|
||||||
|
isResending: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("structures.RoomStatusBar")
|
||||||
|
export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
|
||||||
|
public static contextType = MatrixClientContext;
|
||||||
|
|
||||||
|
constructor(props: IProps, context: typeof MatrixClientContext) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
syncState: this.context.getSyncState(),
|
||||||
|
syncStateData: this.context.getSyncStateData(),
|
||||||
unsentMessages: getUnsentMessages(this.props.room),
|
unsentMessages: getUnsentMessages(this.props.room),
|
||||||
isResending: false,
|
isResending: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
|
||||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
|
|
||||||
|
|
||||||
this._checkSize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
public componentDidMount(): void {
|
||||||
this._checkSize();
|
const client = this.context;
|
||||||
|
client.on("sync", this.onSyncStateChange);
|
||||||
|
client.on("Room.localEchoUpdated", this.onRoomLocalEchoUpdated);
|
||||||
|
|
||||||
|
this.checkSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentDidUpdate(): void {
|
||||||
|
this.checkSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount(): void {
|
||||||
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
|
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
|
||||||
const client = MatrixClientPeg.get();
|
const client = this.context;
|
||||||
if (client) {
|
if (client) {
|
||||||
client.removeListener("sync", this.onSyncStateChange);
|
client.removeListener("sync", this.onSyncStateChange);
|
||||||
client.removeListener("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
|
client.removeListener("Room.localEchoUpdated", this.onRoomLocalEchoUpdated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSyncStateChange = (state, prevState, data) => {
|
private onSyncStateChange = (state: SyncState, prevState: SyncState, data: ISyncStateData): void => {
|
||||||
if (state === "SYNCING" && prevState === "SYNCING") {
|
if (state === "SYNCING" && prevState === "SYNCING") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -113,7 +128,7 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onResendAllClick = () => {
|
private onResendAllClick = (): void => {
|
||||||
Resend.resendUnsentEvents(this.props.room).then(() => {
|
Resend.resendUnsentEvents(this.props.room).then(() => {
|
||||||
this.setState({ isResending: false });
|
this.setState({ isResending: false });
|
||||||
});
|
});
|
||||||
|
@ -121,12 +136,12 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
dis.fire(Action.FocusSendMessageComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onCancelAllClick = () => {
|
private onCancelAllClick = (): void => {
|
||||||
Resend.cancelUnsentEvents(this.props.room);
|
Resend.cancelUnsentEvents(this.props.room);
|
||||||
dis.fire(Action.FocusSendMessageComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => {
|
private onRoomLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
|
||||||
if (room.roomId !== this.props.room.roomId) return;
|
if (room.roomId !== this.props.room.roomId) return;
|
||||||
const messages = getUnsentMessages(this.props.room);
|
const messages = getUnsentMessages(this.props.room);
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -136,8 +151,8 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check whether current size is greater than 0, if yes call props.onVisible
|
// Check whether current size is greater than 0, if yes call props.onVisible
|
||||||
_checkSize() {
|
private checkSize(): void {
|
||||||
if (this._getSize()) {
|
if (this.getSize()) {
|
||||||
if (this.props.onVisible) this.props.onVisible();
|
if (this.props.onVisible) this.props.onVisible();
|
||||||
} else {
|
} else {
|
||||||
if (this.props.onHidden) this.props.onHidden();
|
if (this.props.onHidden) this.props.onHidden();
|
||||||
|
@ -147,8 +162,8 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
// We don't need the actual height - just whether it is likely to have
|
// We don't need the actual height - just whether it is likely to have
|
||||||
// changed - so we use '0' to indicate normal size, and other values to
|
// changed - so we use '0' to indicate normal size, and other values to
|
||||||
// indicate other sizes.
|
// indicate other sizes.
|
||||||
_getSize() {
|
private getSize(): number {
|
||||||
if (this._shouldShowConnectionError()) {
|
if (this.shouldShowConnectionError()) {
|
||||||
return STATUS_BAR_EXPANDED;
|
return STATUS_BAR_EXPANDED;
|
||||||
} else if (this.state.unsentMessages.length > 0 || this.state.isResending) {
|
} else if (this.state.unsentMessages.length > 0 || this.state.isResending) {
|
||||||
return STATUS_BAR_EXPANDED_LARGE;
|
return STATUS_BAR_EXPANDED_LARGE;
|
||||||
|
@ -156,7 +171,7 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
return STATUS_BAR_HIDDEN;
|
return STATUS_BAR_HIDDEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
_shouldShowConnectionError() {
|
private shouldShowConnectionError(): boolean {
|
||||||
// no conn bar trumps the "some not sent" msg since you can't resend without
|
// no conn bar trumps the "some not sent" msg since you can't resend without
|
||||||
// a connection!
|
// a connection!
|
||||||
// There's one situation in which we don't show this 'no connection' bar, and that's
|
// There's one situation in which we don't show this 'no connection' bar, and that's
|
||||||
|
@ -164,12 +179,12 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
const errorIsMauError = Boolean(
|
const errorIsMauError = Boolean(
|
||||||
this.state.syncStateData &&
|
this.state.syncStateData &&
|
||||||
this.state.syncStateData.error &&
|
this.state.syncStateData.error &&
|
||||||
this.state.syncStateData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED',
|
this.state.syncStateData.error.name === 'M_RESOURCE_LIMIT_EXCEEDED',
|
||||||
);
|
);
|
||||||
return this.state.syncState === "ERROR" && !errorIsMauError;
|
return this.state.syncState === "ERROR" && !errorIsMauError;
|
||||||
}
|
}
|
||||||
|
|
||||||
_getUnsentMessageContent() {
|
private getUnsentMessageContent(): JSX.Element {
|
||||||
const unsentMessages = this.state.unsentMessages;
|
const unsentMessages = this.state.unsentMessages;
|
||||||
|
|
||||||
let title;
|
let title;
|
||||||
|
@ -221,10 +236,10 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
let buttonRow = <>
|
let buttonRow = <>
|
||||||
<AccessibleButton onClick={this._onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
|
<AccessibleButton onClick={this.onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
|
||||||
{ _t("Delete all") }
|
{ _t("Delete all") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton onClick={this._onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
|
<AccessibleButton onClick={this.onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
|
||||||
{ _t("Retry all") }
|
{ _t("Retry all") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</>;
|
</>;
|
||||||
|
@ -260,8 +275,8 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
if (this._shouldShowConnectionError()) {
|
if (this.shouldShowConnectionError()) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar">
|
<div className="mx_RoomStatusBar">
|
||||||
<div role="alert">
|
<div role="alert">
|
||||||
|
@ -287,7 +302,7 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.unsentMessages.length > 0 || this.state.isResending) {
|
if (this.state.unsentMessages.length > 0 || this.state.isResending) {
|
||||||
return this._getUnsentMessageContent();
|
return this.getUnsentMessageContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Key } from '../../Keyboard';
|
import { Key } from '../../Keyboard';
|
||||||
import dis from '../../dispatcher/dispatcher';
|
import dis from '../../dispatcher/dispatcher';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
|
@ -24,106 +23,116 @@ import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("structures.SearchBox")
|
interface IProps {
|
||||||
export default class SearchBox extends React.Component {
|
onSearch?: (query: string) => void;
|
||||||
static propTypes = {
|
onCleared?: (source?: string) => void;
|
||||||
onSearch: PropTypes.func,
|
onKeyDown?: (ev: React.KeyboardEvent) => void;
|
||||||
onCleared: PropTypes.func,
|
onFocus?: (ev: React.FocusEvent) => void;
|
||||||
onKeyDown: PropTypes.func,
|
onBlur?: (ev: React.FocusEvent) => void;
|
||||||
className: PropTypes.string,
|
className?: string;
|
||||||
placeholder: PropTypes.string.isRequired,
|
placeholder: string;
|
||||||
autoFocus: PropTypes.bool,
|
blurredPlaceholder?: string;
|
||||||
initialValue: PropTypes.string,
|
autoFocus?: boolean;
|
||||||
|
initialValue?: string;
|
||||||
|
collapsed?: boolean;
|
||||||
|
|
||||||
// If true, the search box will focus and clear itself
|
// If true, the search box will focus and clear itself
|
||||||
// on room search focus action (it would be nicer to take
|
// on room search focus action (it would be nicer to take
|
||||||
// this functionality out, but not obvious how that would work)
|
// this functionality out, but not obvious how that would work)
|
||||||
enableRoomSearchFocus: PropTypes.bool,
|
enableRoomSearchFocus?: boolean;
|
||||||
};
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
interface IState {
|
||||||
|
searchTerm: string;
|
||||||
|
blurred: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("structures.SearchBox")
|
||||||
|
export default class SearchBox extends React.Component<IProps, IState> {
|
||||||
|
private dispatcherRef: string;
|
||||||
|
private search = createRef<HTMLInputElement>();
|
||||||
|
|
||||||
|
static defaultProps: Partial<IProps> = {
|
||||||
enableRoomSearchFocus: false,
|
enableRoomSearchFocus: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this._search = createRef();
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
searchTerm: this.props.initialValue || "",
|
searchTerm: props.initialValue || "",
|
||||||
blurred: true,
|
blurred: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
onAction = payload => {
|
private onAction = (payload): void => {
|
||||||
if (!this.props.enableRoomSearchFocus) return;
|
if (!this.props.enableRoomSearchFocus) return;
|
||||||
|
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'view_room':
|
case 'view_room':
|
||||||
if (this._search.current && payload.clear_search) {
|
if (this.search.current && payload.clear_search) {
|
||||||
this._clearSearch();
|
this.clearSearch();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'focus_room_filter':
|
case 'focus_room_filter':
|
||||||
if (this._search.current) {
|
if (this.search.current) {
|
||||||
this._search.current.focus();
|
this.search.current.focus();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onChange = () => {
|
private onChange = (): void => {
|
||||||
if (!this._search.current) return;
|
if (!this.search.current) return;
|
||||||
this.setState({ searchTerm: this._search.current.value });
|
this.setState({ searchTerm: this.search.current.value });
|
||||||
this.onSearch();
|
this.onSearch();
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearch = throttle(() => {
|
private onSearch = throttle((): void => {
|
||||||
this.props.onSearch(this._search.current.value);
|
this.props.onSearch(this.search.current.value);
|
||||||
}, 200, { trailing: true, leading: true });
|
}, 200, { trailing: true, leading: true });
|
||||||
|
|
||||||
_onKeyDown = ev => {
|
private onKeyDown = (ev: React.KeyboardEvent): void => {
|
||||||
switch (ev.key) {
|
switch (ev.key) {
|
||||||
case Key.ESCAPE:
|
case Key.ESCAPE:
|
||||||
this._clearSearch("keyboard");
|
this.clearSearch("keyboard");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (this.props.onKeyDown) this.props.onKeyDown(ev);
|
if (this.props.onKeyDown) this.props.onKeyDown(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onFocus = ev => {
|
private onFocus = (ev: React.FocusEvent): void => {
|
||||||
this.setState({ blurred: false });
|
this.setState({ blurred: false });
|
||||||
ev.target.select();
|
(ev.target as HTMLInputElement).select();
|
||||||
if (this.props.onFocus) {
|
if (this.props.onFocus) {
|
||||||
this.props.onFocus(ev);
|
this.props.onFocus(ev);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onBlur = ev => {
|
private onBlur = (ev: React.FocusEvent): void => {
|
||||||
this.setState({ blurred: true });
|
this.setState({ blurred: true });
|
||||||
if (this.props.onBlur) {
|
if (this.props.onBlur) {
|
||||||
this.props.onBlur(ev);
|
this.props.onBlur(ev);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_clearSearch(source) {
|
private clearSearch(source?: string): void {
|
||||||
this._search.current.value = "";
|
this.search.current.value = "";
|
||||||
this.onChange();
|
this.onChange();
|
||||||
if (this.props.onCleared) {
|
if (this.props.onCleared) {
|
||||||
this.props.onCleared(source);
|
this.props.onCleared(source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
// check for collapsed here and
|
// check for collapsed here and
|
||||||
// not at parent so we keep
|
// not at parent so we keep
|
||||||
// searchTerm in our state
|
// searchTerm in our state
|
||||||
|
@ -136,7 +145,7 @@ export default class SearchBox extends React.Component {
|
||||||
key="button"
|
key="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="mx_SearchBox_closeButton"
|
className="mx_SearchBox_closeButton"
|
||||||
onClick={() => {this._clearSearch("button"); }}
|
onClick={() => {this.clearSearch("button"); }}
|
||||||
/>) : undefined;
|
/>) : undefined;
|
||||||
|
|
||||||
// show a shorter placeholder when blurred, if requested
|
// show a shorter placeholder when blurred, if requested
|
||||||
|
@ -151,13 +160,13 @@ export default class SearchBox extends React.Component {
|
||||||
<input
|
<input
|
||||||
key="searchfield"
|
key="searchfield"
|
||||||
type="text"
|
type="text"
|
||||||
ref={this._search}
|
ref={this.search}
|
||||||
className={"mx_textinput_icon mx_textinput_search " + className}
|
className={"mx_textinput_icon mx_textinput_search " + className}
|
||||||
value={this.state.searchTerm}
|
value={this.state.searchTerm}
|
||||||
onFocus={this._onFocus}
|
onFocus={this.onFocus}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onKeyDown={this._onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
onBlur={this._onBlur}
|
onBlur={this.onBlur}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoFocus={this.props.autoFocus}
|
autoFocus={this.props.autoFocus}
|
|
@ -58,12 +58,19 @@ import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessi
|
||||||
import { getDisplayAliasForRoom } from "./RoomDirectory";
|
import { getDisplayAliasForRoom } from "./RoomDirectory";
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import { useEventEmitterState } from "../../hooks/useEventEmitter";
|
import { useEventEmitterState } from "../../hooks/useEventEmitter";
|
||||||
|
import { IOOBData } from "../../stores/ThreepidInviteStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
initialText?: string;
|
initialText?: string;
|
||||||
additionalButtons?: ReactNode;
|
additionalButtons?: ReactNode;
|
||||||
showRoom(cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string, autoJoin?: boolean): void;
|
showRoom(
|
||||||
|
cli: MatrixClient,
|
||||||
|
hierarchy: RoomHierarchy,
|
||||||
|
roomId: string,
|
||||||
|
autoJoin?: boolean,
|
||||||
|
roomType?: RoomType,
|
||||||
|
): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ITileProps {
|
interface ITileProps {
|
||||||
|
@ -72,7 +79,7 @@ interface ITileProps {
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
numChildRooms?: number;
|
numChildRooms?: number;
|
||||||
hasPermissions?: boolean;
|
hasPermissions?: boolean;
|
||||||
onViewRoomClick(autoJoin: boolean): void;
|
onViewRoomClick(autoJoin: boolean, roomType: RoomType): void;
|
||||||
onToggleClick?(): void;
|
onToggleClick?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,12 +105,12 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
const onPreviewClick = (ev: ButtonEvent) => {
|
const onPreviewClick = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
onViewRoomClick(false);
|
onViewRoomClick(false, room.room_type as RoomType);
|
||||||
};
|
};
|
||||||
const onJoinClick = (ev: ButtonEvent) => {
|
const onJoinClick = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
onViewRoomClick(true);
|
onViewRoomClick(true, room.room_type as RoomType);
|
||||||
};
|
};
|
||||||
|
|
||||||
let button;
|
let button;
|
||||||
|
@ -280,7 +287,13 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
</li>;
|
</li>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const showRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string, autoJoin = false) => {
|
export const showRoom = (
|
||||||
|
cli: MatrixClient,
|
||||||
|
hierarchy: RoomHierarchy,
|
||||||
|
roomId: string,
|
||||||
|
autoJoin = false,
|
||||||
|
roomType?: RoomType,
|
||||||
|
) => {
|
||||||
const room = hierarchy.roomMap.get(roomId);
|
const room = hierarchy.roomMap.get(roomId);
|
||||||
|
|
||||||
// Don't let the user view a room they won't be able to either peek or join:
|
// Don't let the user view a room they won't be able to either peek or join:
|
||||||
|
@ -305,7 +318,8 @@ export const showRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: st
|
||||||
avatarUrl: room.avatar_url,
|
avatarUrl: room.avatar_url,
|
||||||
// XXX: This logic is duplicated from the JS SDK which would normally decide what the name is.
|
// XXX: This logic is duplicated from the JS SDK which would normally decide what the name is.
|
||||||
name: room.name || roomAlias || _t("Unnamed room"),
|
name: room.name || roomAlias || _t("Unnamed room"),
|
||||||
},
|
roomType,
|
||||||
|
} as IOOBData,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -315,7 +329,7 @@ interface IHierarchyLevelProps {
|
||||||
hierarchy: RoomHierarchy;
|
hierarchy: RoomHierarchy;
|
||||||
parents: Set<string>;
|
parents: Set<string>;
|
||||||
selectedMap?: Map<string, Set<string>>;
|
selectedMap?: Map<string, Set<string>>;
|
||||||
onViewRoomClick(roomId: string, autoJoin: boolean): void;
|
onViewRoomClick(roomId: string, autoJoin: boolean, roomType?: RoomType): void;
|
||||||
onToggleClick?(parentId: string, childId: string): void;
|
onToggleClick?(parentId: string, childId: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -353,8 +367,8 @@ export const HierarchyLevel = ({
|
||||||
room={room}
|
room={room}
|
||||||
suggested={hierarchy.isSuggested(root.room_id, room.room_id)}
|
suggested={hierarchy.isSuggested(root.room_id, room.room_id)}
|
||||||
selected={selectedMap?.get(root.room_id)?.has(room.room_id)}
|
selected={selectedMap?.get(root.room_id)?.has(room.room_id)}
|
||||||
onViewRoomClick={(autoJoin) => {
|
onViewRoomClick={(autoJoin, roomType) => {
|
||||||
onViewRoomClick(room.room_id, autoJoin);
|
onViewRoomClick(room.room_id, autoJoin, roomType);
|
||||||
}}
|
}}
|
||||||
hasPermissions={hasPermissions}
|
hasPermissions={hasPermissions}
|
||||||
onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, room.room_id) : undefined}
|
onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, room.room_id) : undefined}
|
||||||
|
@ -373,8 +387,8 @@ export const HierarchyLevel = ({
|
||||||
}).length}
|
}).length}
|
||||||
suggested={hierarchy.isSuggested(root.room_id, space.room_id)}
|
suggested={hierarchy.isSuggested(root.room_id, space.room_id)}
|
||||||
selected={selectedMap?.get(root.room_id)?.has(space.room_id)}
|
selected={selectedMap?.get(root.room_id)?.has(space.room_id)}
|
||||||
onViewRoomClick={(autoJoin) => {
|
onViewRoomClick={(autoJoin, roomType) => {
|
||||||
onViewRoomClick(space.room_id, autoJoin);
|
onViewRoomClick(space.room_id, autoJoin, roomType);
|
||||||
}}
|
}}
|
||||||
hasPermissions={hasPermissions}
|
hasPermissions={hasPermissions}
|
||||||
onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, space.room_id) : undefined}
|
onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, space.room_id) : undefined}
|
||||||
|
@ -576,7 +590,7 @@ const SpaceHierarchy = ({
|
||||||
const { loading, rooms, hierarchy, loadMore } = useSpaceSummary(space);
|
const { loading, rooms, hierarchy, loadMore } = useSpaceSummary(space);
|
||||||
|
|
||||||
const filteredRoomSet = useMemo<Set<IHierarchyRoom>>(() => {
|
const filteredRoomSet = useMemo<Set<IHierarchyRoom>>(() => {
|
||||||
if (!rooms.length) return new Set();
|
if (!rooms?.length) return new Set();
|
||||||
const lcQuery = query.toLowerCase().trim();
|
const lcQuery = query.toLowerCase().trim();
|
||||||
if (!lcQuery) return new Set(rooms);
|
if (!lcQuery) return new Set(rooms);
|
||||||
|
|
||||||
|
@ -652,8 +666,8 @@ const SpaceHierarchy = ({
|
||||||
parents={new Set()}
|
parents={new Set()}
|
||||||
selectedMap={selected}
|
selectedMap={selected}
|
||||||
onToggleClick={hasPermissions ? onToggleClick : undefined}
|
onToggleClick={hasPermissions ? onToggleClick : undefined}
|
||||||
onViewRoomClick={(roomId, autoJoin) => {
|
onViewRoomClick={(roomId, autoJoin, roomType) => {
|
||||||
showRoom(cli, hierarchy, roomId, autoJoin);
|
showRoom(cli, hierarchy, roomId, autoJoin, roomType);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>;
|
</>;
|
||||||
|
|
|
@ -156,10 +156,10 @@ const SpaceInfo = ({ space }) => {
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBetaClick = () => {
|
const onPreferencesClick = () => {
|
||||||
defaultDispatcher.dispatch({
|
defaultDispatcher.dispatch({
|
||||||
action: Action.ViewUserSettings,
|
action: Action.ViewUserSettings,
|
||||||
initialTabId: UserTab.Labs,
|
initialTabId: UserTab.Preferences,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -286,15 +286,11 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISp
|
||||||
if (!spacesEnabled) {
|
if (!spacesEnabled) {
|
||||||
footer = <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
|
footer = <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
|
||||||
{ myMembership === "join"
|
{ myMembership === "join"
|
||||||
? _t("To view %(spaceName)s, turn on the <a>Spaces beta</a>", {
|
? _t("To view this Space, hide communities in your <a>preferences</a>", {}, {
|
||||||
spaceName: space.name,
|
a: sub => <AccessibleButton onClick={onPreferencesClick} kind="link">{ sub }</AccessibleButton>,
|
||||||
}, {
|
|
||||||
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
|
|
||||||
})
|
})
|
||||||
: _t("To join %(spaceName)s, turn on the <a>Spaces beta</a>", {
|
: _t("To join this Space, hide communities in your <a>preferences</a>", {}, {
|
||||||
spaceName: space.name,
|
a: sub => <AccessibleButton onClick={onPreferencesClick} kind="link">{ sub }</AccessibleButton>,
|
||||||
}, {
|
|
||||||
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -731,7 +727,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx_SpaceRoomView_inviteTeammates_betaDisclaimer">
|
<div className="mx_SpaceRoomView_inviteTeammates_betaDisclaimer">
|
||||||
<BetaPill onClick={onBetaClick} />
|
<BetaPill />
|
||||||
{ _t("<b>This is an experimental feature.</b> For now, " +
|
{ _t("<b>This is an experimental feature.</b> For now, " +
|
||||||
"new users receiving an invite will have to open the invite on <link/> to actually join.", {}, {
|
"new users receiving an invite will have to open the invite on <link/> to actually join.", {}, {
|
||||||
b: sub => <b>{ sub }</b>,
|
b: sub => <b>{ sub }</b>,
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MatrixEvent, Room } from 'matrix-js-sdk/src';
|
import { MatrixEvent, Room } from 'matrix-js-sdk/src';
|
||||||
import { Thread } from 'matrix-js-sdk/src/models/thread';
|
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||||
|
|
||||||
import BaseCard from "../views/right_panel/BaseCard";
|
import BaseCard from "../views/right_panel/BaseCard";
|
||||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||||
|
@ -46,13 +46,13 @@ export default class ThreadPanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
this.room.on("Thread.update", this.onThreadEventReceived);
|
this.room.on(ThreadEvent.Update, this.onThreadEventReceived);
|
||||||
this.room.on("Thread.ready", this.onThreadEventReceived);
|
this.room.on(ThreadEvent.Ready, this.onThreadEventReceived);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
public componentWillUnmount(): void {
|
||||||
this.room.removeListener("Thread.update", this.onThreadEventReceived);
|
this.room.removeListener(ThreadEvent.Update, this.onThreadEventReceived);
|
||||||
this.room.removeListener("Thread.ready", this.onThreadEventReceived);
|
this.room.removeListener(ThreadEvent.Ready, this.onThreadEventReceived);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onThreadEventReceived = () => this.updateThreads();
|
private onThreadEventReceived = () => this.updateThreads();
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MatrixEvent, Room } from 'matrix-js-sdk/src';
|
import { MatrixEvent, Room } from 'matrix-js-sdk/src';
|
||||||
import { Thread } from 'matrix-js-sdk/src/models/thread';
|
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||||
|
|
||||||
import BaseCard from "../views/right_panel/BaseCard";
|
import BaseCard from "../views/right_panel/BaseCard";
|
||||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||||
|
@ -99,15 +99,15 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
thread = new Thread([mxEv], this.props.room, client);
|
thread = new Thread([mxEv], this.props.room, client);
|
||||||
mxEv.setThread(thread);
|
mxEv.setThread(thread);
|
||||||
}
|
}
|
||||||
thread.on("Thread.update", this.updateThread);
|
thread.on(ThreadEvent.Update, this.updateThread);
|
||||||
thread.once("Thread.ready", this.updateThread);
|
thread.once(ThreadEvent.Ready, this.updateThread);
|
||||||
this.updateThread(thread);
|
this.updateThread(thread);
|
||||||
};
|
};
|
||||||
|
|
||||||
private teardownThread = () => {
|
private teardownThread = () => {
|
||||||
if (this.state.thread) {
|
if (this.state.thread) {
|
||||||
this.state.thread.removeListener("Thread.update", this.updateThread);
|
this.state.thread.removeListener(ThreadEvent.Update, this.updateThread);
|
||||||
this.state.thread.removeListener("Thread.ready", this.updateThread);
|
this.state.thread.removeListener(ThreadEvent.Ready, this.updateThread);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -16,52 +16,60 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import * as sdk from "../../index";
|
|
||||||
import Modal from '../../Modal';
|
import Modal from '../../Modal';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import HomePage from "./HomePage";
|
import HomePage from "./HomePage";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||||
|
import MainSplit from "./MainSplit";
|
||||||
|
import RightPanel from "./RightPanel";
|
||||||
|
import Spinner from "../views/elements/Spinner";
|
||||||
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
userId?: string;
|
||||||
|
resizeNotifier: ResizeNotifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
loading: boolean;
|
||||||
|
member?: RoomMember;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.UserView")
|
@replaceableComponent("structures.UserView")
|
||||||
export default class UserView extends React.Component {
|
export default class UserView extends React.Component<IProps, IState> {
|
||||||
static get propTypes() {
|
constructor(props: IProps) {
|
||||||
return {
|
super(props);
|
||||||
userId: PropTypes.string,
|
this.state = {
|
||||||
|
loading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
public componentDidMount(): void {
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (this.props.userId) {
|
if (this.props.userId) {
|
||||||
this._loadProfileInfo();
|
this.loadProfileInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
public componentDidUpdate(prevProps: IProps): void {
|
||||||
// XXX: We shouldn't need to null check the userId here, but we declare
|
// XXX: We shouldn't need to null check the userId here, but we declare
|
||||||
// it as optional and MatrixChat sometimes fires in a way which results
|
// it as optional and MatrixChat sometimes fires in a way which results
|
||||||
// in an NPE when we try to update the profile info.
|
// in an NPE when we try to update the profile info.
|
||||||
if (prevProps.userId !== this.props.userId && this.props.userId) {
|
if (prevProps.userId !== this.props.userId && this.props.userId) {
|
||||||
this._loadProfileInfo();
|
this.loadProfileInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _loadProfileInfo() {
|
private async loadProfileInfo(): Promise<void> {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
let profileInfo;
|
let profileInfo;
|
||||||
try {
|
try {
|
||||||
profileInfo = await cli.getProfileInfo(this.props.userId);
|
profileInfo = await cli.getProfileInfo(this.props.userId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
|
||||||
Modal.createTrackedDialog(_t('Could not load user profile'), '', ErrorDialog, {
|
Modal.createTrackedDialog(_t('Could not load user profile'), '', ErrorDialog, {
|
||||||
title: _t('Could not load user profile'),
|
title: _t('Could not load user profile'),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
|
@ -75,14 +83,11 @@ export default class UserView extends React.Component {
|
||||||
this.setState({ member, loading: false });
|
this.setState({ member, loading: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
|
||||||
return <Spinner />;
|
return <Spinner />;
|
||||||
} else if (this.state.member) {
|
} else if (this.state.member?.user) {
|
||||||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
const panel = <RightPanel user={this.state.member.user} resizeNotifier={this.props.resizeNotifier} />;
|
||||||
const MainSplit = sdk.getComponent('structures.MainSplit');
|
|
||||||
const panel = <RightPanel user={this.state.member} />;
|
|
||||||
return (<MainSplit panel={panel} resizeNotifier={this.props.resizeNotifier}>
|
return (<MainSplit panel={panel} resizeNotifier={this.props.resizeNotifier}>
|
||||||
<HomePage />
|
<HomePage />
|
||||||
</MainSplit>);
|
</MainSplit>);
|
|
@ -17,24 +17,28 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import SyntaxHighlight from "../views/elements/SyntaxHighlight";
|
import SyntaxHighlight from "../views/elements/SyntaxHighlight";
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import * as sdk from "../../index";
|
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog";
|
import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog";
|
||||||
import { canEditContent } from "../../utils/EventUtils";
|
import { canEditContent } from "../../utils/EventUtils";
|
||||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { IDialogProps } from "../views/dialogs/IDialogProps";
|
||||||
|
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
mxEvent: MatrixEvent; // the MatrixEvent associated with the context menu
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
isEditing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.ViewSource")
|
@replaceableComponent("structures.ViewSource")
|
||||||
export default class ViewSource extends React.Component {
|
export default class ViewSource extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
constructor(props: IProps) {
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
mxEvent: PropTypes.object.isRequired, // the MatrixEvent associated with the context menu
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -42,19 +46,20 @@ export default class ViewSource extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onBack() {
|
private onBack(): void {
|
||||||
// TODO: refresh the "Event ID:" modal header
|
// TODO: refresh the "Event ID:" modal header
|
||||||
this.setState({ isEditing: false });
|
this.setState({ isEditing: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
onEdit() {
|
private onEdit(): void {
|
||||||
this.setState({ isEditing: true });
|
this.setState({ isEditing: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the dialog body for viewing the event source
|
// returns the dialog body for viewing the event source
|
||||||
viewSourceContent() {
|
private viewSourceContent(): JSX.Element {
|
||||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||||
const isEncrypted = mxEvent.isEncrypted();
|
const isEncrypted = mxEvent.isEncrypted();
|
||||||
|
// @ts-ignore
|
||||||
const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private
|
const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private
|
||||||
const originalEventSource = mxEvent.event;
|
const originalEventSource = mxEvent.event;
|
||||||
|
|
||||||
|
@ -86,7 +91,7 @@ export default class ViewSource extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the id of the initial message, not the id of the previous edit
|
// returns the id of the initial message, not the id of the previous edit
|
||||||
getBaseEventId() {
|
private getBaseEventId(): string {
|
||||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||||
const isEncrypted = mxEvent.isEncrypted();
|
const isEncrypted = mxEvent.isEncrypted();
|
||||||
const baseMxEvent = this.props.mxEvent;
|
const baseMxEvent = this.props.mxEvent;
|
||||||
|
@ -100,7 +105,7 @@ export default class ViewSource extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the SendCustomEvent component prefilled with the correct details
|
// returns the SendCustomEvent component prefilled with the correct details
|
||||||
editSourceContent() {
|
private editSourceContent(): JSX.Element {
|
||||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||||
|
|
||||||
const isStateEvent = mxEvent.isState();
|
const isStateEvent = mxEvent.isState();
|
||||||
|
@ -159,14 +164,13 @@ export default class ViewSource extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canSendStateEvent(mxEvent) {
|
private canSendStateEvent(mxEvent: MatrixEvent): boolean {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const room = cli.getRoom(mxEvent.getRoomId());
|
const room = cli.getRoom(mxEvent.getRoomId());
|
||||||
return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli);
|
return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
|
|
||||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||||
|
|
||||||
const isEditing = this.state.isEditing;
|
const isEditing = this.state.isEditing;
|
|
@ -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 {
|
||||||
* This component can be used to display generic HTML content in a contextual
|
element: React.ReactNode;
|
||||||
* menu.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@replaceableComponent("views.context_menus.GenericElementContextMenu")
|
|
||||||
export default class GenericElementContextMenu extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
element: PropTypes.element.isRequired,
|
|
||||||
// Function to be called when the parent window is resized
|
// Function to be called when the parent window is resized
|
||||||
// This can be used to reposition or close the menu on resize and
|
// This can be used to reposition or close the menu on resize and
|
||||||
// ensure that it is not displayed in a stale position.
|
// ensure that it is not displayed in a stale position.
|
||||||
onResize: PropTypes.func,
|
onResize?: () => void;
|
||||||
};
|
}
|
||||||
|
|
||||||
constructor(props) {
|
/**
|
||||||
|
* This component can be used to display generic HTML content in a contextual
|
||||||
|
* menu.
|
||||||
|
*/
|
||||||
|
@replaceableComponent("views.context_menus.GenericElementContextMenu")
|
||||||
|
export default class GenericElementContextMenu extends React.Component<IProps> {
|
||||||
|
constructor(props: IProps) {
|
||||||
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 }
|
|
@ -258,7 +258,6 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
placeholder={filterPlaceholder}
|
placeholder={filterPlaceholder}
|
||||||
onSearch={setQuery}
|
onSearch={setQuery}
|
||||||
autoComplete={true}
|
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
<AutoHideScrollbar className="mx_AddExistingToSpace_content">
|
<AutoHideScrollbar className="mx_AddExistingToSpace_content">
|
||||||
|
|
|
@ -243,7 +243,6 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
placeholder={_t("Search for rooms or people")}
|
placeholder={_t("Search for rooms or people")}
|
||||||
onSearch={setQuery}
|
onSearch={setQuery}
|
||||||
autoComplete={true}
|
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
<AutoHideScrollbar className="mx_ForwardList_content">
|
<AutoHideScrollbar className="mx_ForwardList_content">
|
||||||
|
|
|
@ -57,7 +57,6 @@ const SpaceChildPicker = ({ filterPlaceholder, rooms, selected, onChange }) => {
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
placeholder={filterPlaceholder}
|
placeholder={filterPlaceholder}
|
||||||
onSearch={setQuery}
|
onSearch={setQuery}
|
||||||
autoComplete={true}
|
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
<AutoHideScrollbar className="mx_LeaveSpaceDialog_content">
|
<AutoHideScrollbar className="mx_LeaveSpaceDialog_content">
|
||||||
|
|
|
@ -126,7 +126,6 @@ const ManageRestrictedJoinRuleDialog: React.FC<IProps> = ({ room, selected = [],
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
placeholder={_t("Search spaces")}
|
placeholder={_t("Search spaces")}
|
||||||
onSearch={setQuery}
|
onSearch={setQuery}
|
||||||
autoComplete={true}
|
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
<AutoHideScrollbar className="mx_ManageRestrictedJoinRuleDialog_content">
|
<AutoHideScrollbar className="mx_ManageRestrictedJoinRuleDialog_content">
|
||||||
|
|
|
@ -135,7 +135,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
||||||
|
|
||||||
const desc = formatCommaSeparatedList(descs);
|
const desc = formatCommaSeparatedList(descs);
|
||||||
|
|
||||||
return _t('%(nameList)s %(transitionList)s', { nameList: nameList, transitionList: desc });
|
return _t('%(nameList)s %(transitionList)s', { nameList, transitionList: desc });
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!summaries) {
|
if (!summaries) {
|
||||||
|
|
|
@ -106,31 +106,20 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
|
||||||
}
|
}
|
||||||
|
|
||||||
const room = this.context.getRoom(mxEvent.getRoomId());
|
const room = this.context.getRoom(mxEvent.getRoomId());
|
||||||
let label;
|
let label: string;
|
||||||
if (room) {
|
if (room) {
|
||||||
const senders = [];
|
const senders = [];
|
||||||
for (const reactionEvent of reactionEvents) {
|
for (const reactionEvent of reactionEvents) {
|
||||||
const member = room.getMember(reactionEvent.getSender());
|
const member = room.getMember(reactionEvent.getSender());
|
||||||
const name = member ? member.name : reactionEvent.getSender();
|
senders.push(member?.name || reactionEvent.getSender());
|
||||||
senders.push(name);
|
|
||||||
}
|
}
|
||||||
label = _t(
|
|
||||||
"<reactors/><reactedWith> reacted with %(content)s</reactedWith>",
|
const reactors = formatCommaSeparatedList(senders, 6);
|
||||||
{
|
if (content) {
|
||||||
content,
|
label = _t("%(reactors)s reacted with %(content)s", { reactors, content });
|
||||||
},
|
} else {
|
||||||
{
|
label = reactors;
|
||||||
reactors: () => {
|
|
||||||
return formatCommaSeparatedList(senders, 6);
|
|
||||||
},
|
|
||||||
reactedWith: (sub) => {
|
|
||||||
if (!content) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
return sub;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const isPeeking = room.getMyMembership() !== "join";
|
const isPeeking = room.getMyMembership() !== "join";
|
||||||
return <AccessibleButton
|
return <AccessibleButton
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { Relations } from "matrix-js-sdk/src/models/relations";
|
import { Relations } from "matrix-js-sdk/src/models/relations";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { Thread } from 'matrix-js-sdk/src/models/thread';
|
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||||
|
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -464,8 +464,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SettingsStore.getValue("feature_thread")) {
|
if (SettingsStore.getValue("feature_thread")) {
|
||||||
this.props.mxEvent.once("Thread.ready", this.updateThread);
|
this.props.mxEvent.once(ThreadEvent.Ready, this.updateThread);
|
||||||
this.props.mxEvent.on("Thread.update", this.updateThread);
|
this.props.mxEvent.on(ThreadEvent.Update, this.updateThread);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1192,7 +1192,11 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
const thread = ReplyThread.makeThread(
|
let thread;
|
||||||
|
// When the "showHiddenEventsInTimeline" lab is enabled,
|
||||||
|
// avoid showing replies for hidden events (events without tiles)
|
||||||
|
if (haveTileForEvent(this.props.mxEvent)) {
|
||||||
|
thread = ReplyThread.makeThread(
|
||||||
this.props.mxEvent,
|
this.props.mxEvent,
|
||||||
this.props.onHeightChanged,
|
this.props.onHeightChanged,
|
||||||
this.props.permalinkCreator,
|
this.props.permalinkCreator,
|
||||||
|
@ -1200,6 +1204,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
this.props.layout,
|
this.props.layout,
|
||||||
this.props.alwaysShowTimestamps || this.state.hover,
|
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
|
||||||
|
|
|
@ -14,8 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import PropTypes from 'prop-types';
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||||
|
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
import { IJoinRuleEventContent, JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
|
@ -27,91 +32,102 @@ import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore
|
||||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import InviteReason from "../elements/InviteReason";
|
import InviteReason from "../elements/InviteReason";
|
||||||
|
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||||
|
import Spinner from "../elements/Spinner";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
|
||||||
const MemberEventHtmlReasonField = "io.element.html_reason";
|
const MemberEventHtmlReasonField = "io.element.html_reason";
|
||||||
|
|
||||||
const MessageCase = Object.freeze({
|
enum MessageCase {
|
||||||
NotLoggedIn: "NotLoggedIn",
|
NotLoggedIn = "NotLoggedIn",
|
||||||
Joining: "Joining",
|
Joining = "Joining",
|
||||||
Loading: "Loading",
|
Loading = "Loading",
|
||||||
Rejecting: "Rejecting",
|
Rejecting = "Rejecting",
|
||||||
Kicked: "Kicked",
|
Kicked = "Kicked",
|
||||||
Banned: "Banned",
|
Banned = "Banned",
|
||||||
OtherThreePIDError: "OtherThreePIDError",
|
OtherThreePIDError = "OtherThreePIDError",
|
||||||
InvitedEmailNotFoundInAccount: "InvitedEmailNotFoundInAccount",
|
InvitedEmailNotFoundInAccount = "InvitedEmailNotFoundInAccount",
|
||||||
InvitedEmailNoIdentityServer: "InvitedEmailNoIdentityServer",
|
InvitedEmailNoIdentityServer = "InvitedEmailNoIdentityServer",
|
||||||
InvitedEmailMismatch: "InvitedEmailMismatch",
|
InvitedEmailMismatch = "InvitedEmailMismatch",
|
||||||
Invite: "Invite",
|
Invite = "Invite",
|
||||||
ViewingRoom: "ViewingRoom",
|
ViewingRoom = "ViewingRoom",
|
||||||
RoomNotFound: "RoomNotFound",
|
RoomNotFound = "RoomNotFound",
|
||||||
OtherError: "OtherError",
|
OtherError = "OtherError",
|
||||||
});
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.RoomPreviewBar")
|
interface IProps {
|
||||||
export default class RoomPreviewBar extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onJoinClick: PropTypes.func,
|
|
||||||
onRejectClick: PropTypes.func,
|
|
||||||
onRejectAndIgnoreClick: PropTypes.func,
|
|
||||||
onForgetClick: PropTypes.func,
|
|
||||||
// if inviterName is specified, the preview bar will shown an invite to the room.
|
// if inviterName is specified, the preview bar will shown an invite to the room.
|
||||||
// You should also specify onRejectClick if specifiying inviterName
|
// You should also specify onRejectClick if specifying inviterName
|
||||||
inviterName: PropTypes.string,
|
inviterName?: string;
|
||||||
|
|
||||||
// If invited by 3rd party invite, the email address the invite was sent to
|
// If invited by 3rd party invite, the email address the invite was sent to
|
||||||
invitedEmail: PropTypes.string,
|
invitedEmail?: string;
|
||||||
|
|
||||||
// For third party invites, information passed about the room out-of-band
|
// For third party invites, information passed about the room out-of-band
|
||||||
oobData: PropTypes.object,
|
oobData?: IOOBData;
|
||||||
|
|
||||||
// For third party invites, a URL for a 3pid invite signing service
|
// For third party invites, a URL for a 3pid invite signing service
|
||||||
signUrl: PropTypes.string,
|
signUrl?: string;
|
||||||
|
|
||||||
// A standard client/server API error object. If supplied, indicates that the
|
// A standard client/server API error object. If supplied, indicates that the
|
||||||
// caller was unable to fetch details about the room for the given reason.
|
// caller was unable to fetch details about the room for the given reason.
|
||||||
error: PropTypes.object,
|
error?: MatrixError;
|
||||||
|
|
||||||
canPreview: PropTypes.bool,
|
canPreview?: boolean;
|
||||||
previewLoading: PropTypes.bool,
|
previewLoading?: boolean;
|
||||||
room: PropTypes.object,
|
room?: Room;
|
||||||
|
|
||||||
// When a spinner is present, a spinnerState can be specified to indicate the
|
loading?: boolean;
|
||||||
// purpose of the spinner.
|
joining?: boolean;
|
||||||
spinner: PropTypes.bool,
|
rejecting?: boolean;
|
||||||
spinnerState: PropTypes.oneOf(["joining"]),
|
|
||||||
loading: PropTypes.bool,
|
|
||||||
joining: PropTypes.bool,
|
|
||||||
rejecting: PropTypes.bool,
|
|
||||||
// The alias that was used to access this room, if appropriate
|
// The alias that was used to access this room, if appropriate
|
||||||
// If given, this will be how the room is referred to (eg.
|
// If given, this will be how the room is referred to (eg.
|
||||||
// in error messages).
|
// in error messages).
|
||||||
roomAlias: PropTypes.string,
|
roomAlias?: string;
|
||||||
};
|
|
||||||
|
|
||||||
|
onJoinClick?(): void;
|
||||||
|
onRejectClick?(): void;
|
||||||
|
onRejectAndIgnoreClick?(): void;
|
||||||
|
onForgetClick?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
busy: boolean;
|
||||||
|
accountEmails?: string[];
|
||||||
|
invitedEmailMxid?: string;
|
||||||
|
threePidFetchError?: MatrixError;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.rooms.RoomPreviewBar")
|
||||||
|
export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onJoinClick() {},
|
onJoinClick() {},
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
busy: false,
|
busy: false,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this._checkInvitedEmail();
|
this.checkInvitedEmail();
|
||||||
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this._onCommunityUpdate);
|
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this.onCommunityUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
if (this.props.invitedEmail !== prevProps.invitedEmail || this.props.inviterName !== prevProps.inviterName) {
|
if (this.props.invitedEmail !== prevProps.invitedEmail || this.props.inviterName !== prevProps.inviterName) {
|
||||||
this._checkInvitedEmail();
|
this.checkInvitedEmail();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this._onCommunityUpdate);
|
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this.onCommunityUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _checkInvitedEmail() {
|
private async checkInvitedEmail() {
|
||||||
// If this is an invite and we've been told what email address was
|
// If this is an invite and we've been told what email address was
|
||||||
// invited, fetch the user's account emails and discovery bindings so we
|
// invited, fetch the user's account emails and discovery bindings so we
|
||||||
// can check them against the email that was invited.
|
// can check them against the email that was invited.
|
||||||
|
@ -121,8 +137,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
// Gather the account 3PIDs
|
// Gather the account 3PIDs
|
||||||
const account3pids = await MatrixClientPeg.get().getThreePids();
|
const account3pids = await MatrixClientPeg.get().getThreePids();
|
||||||
this.setState({
|
this.setState({
|
||||||
accountEmails: account3pids.threepids
|
accountEmails: account3pids.threepids.filter(b => b.medium === 'email').map(b => b.address),
|
||||||
.filter(b => b.medium === 'email').map(b => b.address),
|
|
||||||
});
|
});
|
||||||
// If we have an IS connected, use that to lookup the email and
|
// If we have an IS connected, use that to lookup the email and
|
||||||
// check the bound MXID.
|
// check the bound MXID.
|
||||||
|
@ -146,21 +161,21 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCommunityUpdate = (roomId) => {
|
private onCommunityUpdate = (roomId: string): void => {
|
||||||
if (this.props.room && this.props.room.roomId !== roomId) {
|
if (this.props.room && this.props.room.roomId !== roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.forceUpdate(); // we have nothing to update
|
this.forceUpdate(); // we have nothing to update
|
||||||
};
|
};
|
||||||
|
|
||||||
_getMessageCase() {
|
private getMessageCase(): MessageCase {
|
||||||
const isGuest = MatrixClientPeg.get().isGuest();
|
const isGuest = MatrixClientPeg.get().isGuest();
|
||||||
|
|
||||||
if (isGuest) {
|
if (isGuest) {
|
||||||
return MessageCase.NotLoggedIn;
|
return MessageCase.NotLoggedIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
const myMember = this._getMyMember();
|
const myMember = this.getMyMember();
|
||||||
|
|
||||||
if (myMember) {
|
if (myMember) {
|
||||||
if (myMember.isKicked()) {
|
if (myMember.isKicked()) {
|
||||||
|
@ -195,7 +210,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
}
|
}
|
||||||
return MessageCase.Invite;
|
return MessageCase.Invite;
|
||||||
} else if (this.props.error) {
|
} else if (this.props.error) {
|
||||||
if (this.props.error.errcode == 'M_NOT_FOUND') {
|
if ((this.props.error as MatrixError).errcode == 'M_NOT_FOUND') {
|
||||||
return MessageCase.RoomNotFound;
|
return MessageCase.RoomNotFound;
|
||||||
} else {
|
} else {
|
||||||
return MessageCase.OtherError;
|
return MessageCase.OtherError;
|
||||||
|
@ -205,8 +220,8 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getKickOrBanInfo() {
|
private getKickOrBanInfo(): { memberName?: string, reason?: string } {
|
||||||
const myMember = this._getMyMember();
|
const myMember = this.getMyMember();
|
||||||
if (!myMember) {
|
if (!myMember) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -219,24 +234,19 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
return { memberName, reason };
|
return { memberName, reason };
|
||||||
}
|
}
|
||||||
|
|
||||||
_joinRule() {
|
private joinRule(): JoinRule {
|
||||||
const room = this.props.room;
|
return this.props.room?.currentState
|
||||||
if (room) {
|
.getStateEvents(EventType.RoomJoinRules, "")?.getContent<IJoinRuleEventContent>().join_rule;
|
||||||
const joinRules = room.currentState.getStateEvents('m.room.join_rules', '');
|
|
||||||
if (joinRules) {
|
|
||||||
return joinRules.getContent().join_rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_communityProfile() {
|
private communityProfile(): { displayName?: string, avatarMxc?: string } {
|
||||||
if (this.props.room) return CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId);
|
if (this.props.room) return CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId);
|
||||||
return { displayName: null, avatarMxc: null };
|
return { displayName: null, avatarMxc: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
_roomName(atStart = false) {
|
private roomName(atStart = false): string {
|
||||||
let name = this.props.room ? this.props.room.name : this.props.roomAlias;
|
let name = this.props.room ? this.props.room.name : this.props.roomAlias;
|
||||||
const profile = this._communityProfile();
|
const profile = this.communityProfile();
|
||||||
if (profile.displayName) name = profile.displayName;
|
if (profile.displayName) name = profile.displayName;
|
||||||
if (name) {
|
if (name) {
|
||||||
return name;
|
return name;
|
||||||
|
@ -247,14 +257,11 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getMyMember() {
|
private getMyMember(): RoomMember {
|
||||||
return (
|
return this.props.room?.getMember(MatrixClientPeg.get().getUserId());
|
||||||
this.props.room &&
|
|
||||||
this.props.room.getMember(MatrixClientPeg.get().getUserId())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_getInviteMember() {
|
private getInviteMember(): RoomMember {
|
||||||
const { room } = this.props;
|
const { room } = this.props;
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return;
|
return;
|
||||||
|
@ -268,8 +275,8 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
return room.currentState.getMember(inviterUserId);
|
return room.currentState.getMember(inviterUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_isDMInvite() {
|
private isDMInvite(): boolean {
|
||||||
const myMember = this._getMyMember();
|
const myMember = this.getMyMember();
|
||||||
if (!myMember) {
|
if (!myMember) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -278,7 +285,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
return memberContent.membership === "invite" && memberContent.is_direct;
|
return memberContent.membership === "invite" && memberContent.is_direct;
|
||||||
}
|
}
|
||||||
|
|
||||||
_makeScreenAfterLogin() {
|
private makeScreenAfterLogin(): { screen: string, params: Record<string, any> } {
|
||||||
return {
|
return {
|
||||||
screen: 'room',
|
screen: 'room',
|
||||||
params: {
|
params: {
|
||||||
|
@ -291,18 +298,16 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoginClick = () => {
|
private onLoginClick = () => {
|
||||||
dis.dispatch({ action: 'start_login', screenAfterLogin: this._makeScreenAfterLogin() });
|
dis.dispatch({ action: 'start_login', screenAfterLogin: this.makeScreenAfterLogin() });
|
||||||
};
|
};
|
||||||
|
|
||||||
onRegisterClick = () => {
|
private onRegisterClick = () => {
|
||||||
dis.dispatch({ action: 'start_registration', screenAfterLogin: this._makeScreenAfterLogin() });
|
dis.dispatch({ action: 'start_registration', screenAfterLogin: this.makeScreenAfterLogin() });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
const Spinner = sdk.getComponent('elements.Spinner');
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
|
|
||||||
let showSpinner = false;
|
let showSpinner = false;
|
||||||
let title;
|
let title;
|
||||||
|
@ -315,10 +320,10 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
let footer;
|
let footer;
|
||||||
const extraComponents = [];
|
const extraComponents = [];
|
||||||
|
|
||||||
const messageCase = this._getMessageCase();
|
const messageCase = this.getMessageCase();
|
||||||
switch (messageCase) {
|
switch (messageCase) {
|
||||||
case MessageCase.Joining: {
|
case MessageCase.Joining: {
|
||||||
title = _t("Joining room …");
|
title = this.props.oobData.roomType === RoomType.Space ? _t("Joining space …") : _t("Joining room …");
|
||||||
showSpinner = true;
|
showSpinner = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -349,12 +354,12 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MessageCase.Kicked: {
|
case MessageCase.Kicked: {
|
||||||
const { memberName, reason } = this._getKickOrBanInfo();
|
const { memberName, reason } = this.getKickOrBanInfo();
|
||||||
title = _t("You were kicked from %(roomName)s by %(memberName)s",
|
title = _t("You were kicked from %(roomName)s by %(memberName)s",
|
||||||
{ memberName, roomName: this._roomName() });
|
{ memberName, roomName: this.roomName() });
|
||||||
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
|
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
|
||||||
|
|
||||||
if (this._joinRule() === "invite") {
|
if (this.joinRule() === "invite") {
|
||||||
primaryActionLabel = _t("Forget this room");
|
primaryActionLabel = _t("Forget this room");
|
||||||
primaryActionHandler = this.props.onForgetClick;
|
primaryActionHandler = this.props.onForgetClick;
|
||||||
} else {
|
} else {
|
||||||
|
@ -366,9 +371,9 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MessageCase.Banned: {
|
case MessageCase.Banned: {
|
||||||
const { memberName, reason } = this._getKickOrBanInfo();
|
const { memberName, reason } = this.getKickOrBanInfo();
|
||||||
title = _t("You were banned from %(roomName)s by %(memberName)s",
|
title = _t("You were banned from %(roomName)s by %(memberName)s",
|
||||||
{ memberName, roomName: this._roomName() });
|
{ memberName, roomName: this.roomName() });
|
||||||
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
|
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
|
||||||
primaryActionLabel = _t("Forget this room");
|
primaryActionLabel = _t("Forget this room");
|
||||||
primaryActionHandler = this.props.onForgetClick;
|
primaryActionHandler = this.props.onForgetClick;
|
||||||
|
@ -376,8 +381,8 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
}
|
}
|
||||||
case MessageCase.OtherThreePIDError: {
|
case MessageCase.OtherThreePIDError: {
|
||||||
title = _t("Something went wrong with your invite to %(roomName)s",
|
title = _t("Something went wrong with your invite to %(roomName)s",
|
||||||
{ roomName: this._roomName() });
|
{ roomName: this.roomName() });
|
||||||
const joinRule = this._joinRule();
|
const joinRule = this.joinRule();
|
||||||
const errCodeMessage = _t(
|
const errCodeMessage = _t(
|
||||||
"An error (%(errcode)s) was returned while trying to validate your " +
|
"An error (%(errcode)s) was returned while trying to validate your " +
|
||||||
"invite. You could try to pass this information on to a room admin.",
|
"invite. You could try to pass this information on to a room admin.",
|
||||||
|
@ -410,7 +415,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
"This invite to %(roomName)s was sent to %(email)s which is not " +
|
"This invite to %(roomName)s was sent to %(email)s which is not " +
|
||||||
"associated with your account",
|
"associated with your account",
|
||||||
{
|
{
|
||||||
roomName: this._roomName(),
|
roomName: this.roomName(),
|
||||||
email: this.props.invitedEmail,
|
email: this.props.invitedEmail,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -427,7 +432,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
title = _t(
|
title = _t(
|
||||||
"This invite to %(roomName)s was sent to %(email)s",
|
"This invite to %(roomName)s was sent to %(email)s",
|
||||||
{
|
{
|
||||||
roomName: this._roomName(),
|
roomName: this.roomName(),
|
||||||
email: this.props.invitedEmail,
|
email: this.props.invitedEmail,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -443,7 +448,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
title = _t(
|
title = _t(
|
||||||
"This invite to %(roomName)s was sent to %(email)s",
|
"This invite to %(roomName)s was sent to %(email)s",
|
||||||
{
|
{
|
||||||
roomName: this._roomName(),
|
roomName: this.roomName(),
|
||||||
email: this.props.invitedEmail,
|
email: this.props.invitedEmail,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -458,11 +463,11 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
case MessageCase.Invite: {
|
case MessageCase.Invite: {
|
||||||
const RoomAvatar = sdk.getComponent("views.avatars.RoomAvatar");
|
const RoomAvatar = sdk.getComponent("views.avatars.RoomAvatar");
|
||||||
const oobData = Object.assign({}, this.props.oobData, {
|
const oobData = Object.assign({}, this.props.oobData, {
|
||||||
avatarUrl: this._communityProfile().avatarMxc,
|
avatarUrl: this.communityProfile().avatarMxc,
|
||||||
});
|
});
|
||||||
const avatar = <RoomAvatar room={this.props.room} oobData={oobData} />;
|
const avatar = <RoomAvatar room={this.props.room} oobData={oobData} />;
|
||||||
|
|
||||||
const inviteMember = this._getInviteMember();
|
const inviteMember = this.getInviteMember();
|
||||||
let inviterElement;
|
let inviterElement;
|
||||||
if (inviteMember) {
|
if (inviteMember) {
|
||||||
inviterElement = <span>
|
inviterElement = <span>
|
||||||
|
@ -474,7 +479,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
inviterElement = (<span className="mx_RoomPreviewBar_inviter">{ this.props.inviterName }</span>);
|
inviterElement = (<span className="mx_RoomPreviewBar_inviter">{ this.props.inviterName }</span>);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDM = this._isDMInvite();
|
const isDM = this.isDMInvite();
|
||||||
if (isDM) {
|
if (isDM) {
|
||||||
title = _t("Do you want to chat with %(user)s?",
|
title = _t("Do you want to chat with %(user)s?",
|
||||||
{ user: inviteMember.name });
|
{ user: inviteMember.name });
|
||||||
|
@ -485,7 +490,7 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
primaryActionLabel = _t("Start chatting");
|
primaryActionLabel = _t("Start chatting");
|
||||||
} else {
|
} else {
|
||||||
title = _t("Do you want to join %(roomName)s?",
|
title = _t("Do you want to join %(roomName)s?",
|
||||||
{ roomName: this._roomName() });
|
{ roomName: this.roomName() });
|
||||||
subTitle = [
|
subTitle = [
|
||||||
avatar,
|
avatar,
|
||||||
_t("<userName/> invited you", {}, { userName: () => inviterElement }),
|
_t("<userName/> invited you", {}, { userName: () => inviterElement }),
|
||||||
|
@ -519,22 +524,22 @@ export default class RoomPreviewBar extends React.Component {
|
||||||
case MessageCase.ViewingRoom: {
|
case MessageCase.ViewingRoom: {
|
||||||
if (this.props.canPreview) {
|
if (this.props.canPreview) {
|
||||||
title = _t("You're previewing %(roomName)s. Want to join it?",
|
title = _t("You're previewing %(roomName)s. Want to join it?",
|
||||||
{ roomName: this._roomName() });
|
{ roomName: this.roomName() });
|
||||||
} else {
|
} else {
|
||||||
title = _t("%(roomName)s can't be previewed. Do you want to join it?",
|
title = _t("%(roomName)s can't be previewed. Do you want to join it?",
|
||||||
{ roomName: this._roomName(true) });
|
{ roomName: this.roomName(true) });
|
||||||
}
|
}
|
||||||
primaryActionLabel = _t("Join the discussion");
|
primaryActionLabel = _t("Join the discussion");
|
||||||
primaryActionHandler = this.props.onJoinClick;
|
primaryActionHandler = this.props.onJoinClick;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MessageCase.RoomNotFound: {
|
case MessageCase.RoomNotFound: {
|
||||||
title = _t("%(roomName)s does not exist.", { roomName: this._roomName(true) });
|
title = _t("%(roomName)s does not exist.", { roomName: this.roomName(true) });
|
||||||
subTitle = _t("This room doesn't exist. Are you sure you're at the right place?");
|
subTitle = _t("This room doesn't exist. Are you sure you're at the right place?");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MessageCase.OtherError: {
|
case MessageCase.OtherError: {
|
||||||
title = _t("%(roomName)s is not accessible at this time.", { roomName: this._roomName(true) });
|
title = _t("%(roomName)s is not accessible at this time.", { roomName: this.roomName(true) });
|
||||||
subTitle = [
|
subTitle = [
|
||||||
_t("Try again later, or ask a room admin to check if you have access."),
|
_t("Try again later, or ask a room admin to check if you have access."),
|
||||||
_t(
|
_t(
|
|
@ -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();
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog";
|
import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog";
|
||||||
import RoomUpgradeWarningDialog from "../dialogs/RoomUpgradeWarningDialog";
|
import RoomUpgradeWarningDialog from "../dialogs/RoomUpgradeWarningDialog";
|
||||||
|
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
import { upgradeRoom } from "../../../utils/RoomUpgrade";
|
import { upgradeRoom } from "../../../utils/RoomUpgrade";
|
||||||
import { arrayHasDiff } from "../../../utils/arrays";
|
import { arrayHasDiff } from "../../../utils/arrays";
|
||||||
import { useLocalEcho } from "../../../hooks/useLocalEcho";
|
import { useLocalEcho } from "../../../hooks/useLocalEcho";
|
||||||
|
@ -207,13 +208,37 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
|
||||||
} else if (preferredRestrictionVersion) {
|
} else if (preferredRestrictionVersion) {
|
||||||
// Block this action on a room upgrade otherwise it'd make their room unjoinable
|
// Block this action on a room upgrade otherwise it'd make their room unjoinable
|
||||||
const targetVersion = preferredRestrictionVersion;
|
const targetVersion = preferredRestrictionVersion;
|
||||||
Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, {
|
|
||||||
|
const modal = Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, {
|
||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
targetVersion,
|
targetVersion,
|
||||||
description: _t("This upgrade will allow members of selected spaces " +
|
description: _t("This upgrade will allow members of selected spaces " +
|
||||||
"access to this room without an invite."),
|
"access to this room without an invite."),
|
||||||
onFinished: async (resp) => {
|
});
|
||||||
|
|
||||||
|
const [resp] = await modal.finished;
|
||||||
if (!resp?.continue) return;
|
if (!resp?.continue) return;
|
||||||
|
|
||||||
|
const userId = cli.getUserId();
|
||||||
|
const unableToUpdateSomeParents = Array.from(SpaceStore.instance.getKnownParents(room.roomId))
|
||||||
|
.some(roomId => !cli.getRoom(roomId)?.currentState.maySendStateEvent(EventType.SpaceChild, userId));
|
||||||
|
if (unableToUpdateSomeParents) {
|
||||||
|
const modal = Modal.createTrackedDialog<[boolean]>('Parent relink warning', '', QuestionDialog, {
|
||||||
|
title: _t("Before you upgrade"),
|
||||||
|
description: (
|
||||||
|
<div>{ _t("This room is in some spaces you’re not an admin of. " +
|
||||||
|
"In those spaces, the old room will still be shown, " +
|
||||||
|
"but people will be prompted to join the new one.") }</div>
|
||||||
|
),
|
||||||
|
hasCancelButton: true,
|
||||||
|
button: _t("Upgrade anyway"),
|
||||||
|
danger: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [shouldUpgrade] = await modal.finished;
|
||||||
|
if (!shouldUpgrade) return;
|
||||||
|
}
|
||||||
|
|
||||||
const roomId = await upgradeRoom(room, targetVersion, resp.invite, true, true, true);
|
const roomId = await upgradeRoom(room, targetVersion, resp.invite, true, true, true);
|
||||||
closeSettingsFn();
|
closeSettingsFn();
|
||||||
// switch to the new room in the background
|
// switch to the new room in the background
|
||||||
|
@ -226,8 +251,7 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
|
||||||
action: "open_room_settings",
|
action: "open_room_settings",
|
||||||
initial_tab_id: ROOM_SECURITY_TAB,
|
initial_tab_id: ROOM_SECURITY_TAB,
|
||||||
});
|
});
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,6 @@ import { replaceableComponent } from "../../../../../utils/replaceableComponent"
|
||||||
import SettingsFlag from '../../../elements/SettingsFlag';
|
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||||
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
|
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
import SpaceStore from "../../../../../stores/SpaceStore";
|
|
||||||
import GroupAvatar from "../../../avatars/GroupAvatar";
|
import GroupAvatar from "../../../avatars/GroupAvatar";
|
||||||
import dis from "../../../../../dispatcher/dispatcher";
|
import dis from "../../../../../dispatcher/dispatcher";
|
||||||
import GroupActions from "../../../../../actions/GroupActions";
|
import GroupActions from "../../../../../actions/GroupActions";
|
||||||
|
@ -145,7 +144,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
];
|
];
|
||||||
|
|
||||||
static COMMUNITIES_SETTINGS = [
|
static COMMUNITIES_SETTINGS = [
|
||||||
// TODO: part of delabsing move the toggle here - https://github.com/vector-im/element-web/issues/18088
|
"showCommunitiesInsteadOfSpaces",
|
||||||
];
|
];
|
||||||
|
|
||||||
static KEYBINDINGS_SETTINGS = [
|
static KEYBINDINGS_SETTINGS = [
|
||||||
|
@ -286,9 +285,17 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value);
|
SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderGroup(settingIds: string[]): React.ReactNodeArray {
|
private renderGroup(
|
||||||
return settingIds.filter(SettingsStore.isEnabled).map(i => {
|
settingIds: string[],
|
||||||
return <SettingsFlag key={i} name={i} level={SettingLevel.ACCOUNT} />;
|
level = SettingLevel.ACCOUNT,
|
||||||
|
includeDisabled = false,
|
||||||
|
): React.ReactNodeArray {
|
||||||
|
if (!includeDisabled) {
|
||||||
|
settingIds = settingIds.filter(SettingsStore.isEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
return settingIds.map(i => {
|
||||||
|
return <SettingsFlag key={i} name={i} level={level} />;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,10 +341,10 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
{ this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
|
{ this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ SpaceStore.spacesEnabled && <div className="mx_SettingsTab_section">
|
<div className="mx_SettingsTab_section">
|
||||||
<span className="mx_SettingsTab_subheading">{ _t("Spaces") }</span>
|
<span className="mx_SettingsTab_subheading">{ _t("Spaces") }</span>
|
||||||
{ this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS) }
|
{ this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS, SettingLevel.ACCOUNT, true) }
|
||||||
</div> }
|
</div>
|
||||||
|
|
||||||
<div className="mx_SettingsTab_section">
|
<div className="mx_SettingsTab_section">
|
||||||
<span className="mx_SettingsTab_subheading">{ _t("Communities") }</span>
|
<span className="mx_SettingsTab_subheading">{ _t("Communities") }</span>
|
||||||
|
@ -349,7 +356,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
<p>{ _t("If a community isn't shown you may not have permission to convert it.") }</p>
|
<p>{ _t("If a community isn't shown you may not have permission to convert it.") }</p>
|
||||||
<CommunityMigrator onFinished={this.props.closeSettingsFn} />
|
<CommunityMigrator onFinished={this.props.closeSettingsFn} />
|
||||||
</details>
|
</details>
|
||||||
{ this.renderGroup(PreferencesUserSettingsTab.COMMUNITIES_SETTINGS) }
|
{ this.renderGroup(PreferencesUserSettingsTab.COMMUNITIES_SETTINGS, SettingLevel.DEVICE) }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx_SettingsTab_section">
|
<div className="mx_SettingsTab_section">
|
||||||
|
|
|
@ -117,9 +117,7 @@ export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
|
||||||
"Your feedback will help inform the next versions."),
|
"Your feedback will help inform the next versions."),
|
||||||
rageshakeLabel: "spaces-feedback",
|
rageshakeLabel: "spaces-feedback",
|
||||||
rageshakeData: Object.fromEntries([
|
rageshakeData: Object.fromEntries([
|
||||||
"feature_spaces.all_rooms",
|
"Spaces.allRoomsInHome",
|
||||||
"feature_spaces.space_member_dms",
|
|
||||||
"feature_spaces.space_dm_badges",
|
|
||||||
].map(k => [k, SettingsStore.getValue(k)])),
|
].map(k => [k, SettingsStore.getValue(k)])),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
@ -301,13 +299,13 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{ _t("You can also create a Space from a <a>community</a>.", {}, {
|
{ _t("You can also make Spaces from <a>communities</a>.", {}, {
|
||||||
a: sub => <AccessibleButton kind="link" onClick={onCreateSpaceFromCommunityClick}>
|
a: sub => <AccessibleButton kind="link" onClick={onCreateSpaceFromCommunityClick}>
|
||||||
{ sub }
|
{ sub }
|
||||||
</AccessibleButton>,
|
</AccessibleButton>,
|
||||||
}) }
|
}) }
|
||||||
<br />
|
<br />
|
||||||
{ _t("To join an existing space you'll need an invite.") }
|
{ _t("To join a space you'll need an invite.") }
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SpaceFeedbackPrompt onClick={onFinished} />
|
<SpaceFeedbackPrompt onClick={onFinished} />
|
||||||
|
|
|
@ -151,12 +151,19 @@ const CreateSpaceButton = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const onNewClick = menuDisplayed ? closeMenu : () => {
|
const onNewClick = menuDisplayed ? closeMenu : () => {
|
||||||
|
// persist that the user has interacted with this, use it to dismiss the beta dot
|
||||||
|
localStorage.setItem("mx_seenSpaces", "1");
|
||||||
if (!isPanelCollapsed) setPanelCollapsed(true);
|
if (!isPanelCollapsed) setPanelCollapsed(true);
|
||||||
openMenu();
|
openMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let betaDot: JSX.Element;
|
||||||
|
if (!localStorage.getItem("mx_seenSpaces") && !SpaceStore.instance.spacePanelSpaces.length) {
|
||||||
|
betaDot = <div className="mx_BetaDot" />;
|
||||||
|
}
|
||||||
|
|
||||||
return <li
|
return <li
|
||||||
className={classNames("mx_SpaceItem", {
|
className={classNames("mx_SpaceItem mx_SpaceItem_new", {
|
||||||
"collapsed": isPanelCollapsed,
|
"collapsed": isPanelCollapsed,
|
||||||
})}
|
})}
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
|
@ -169,6 +176,7 @@ const CreateSpaceButton = ({
|
||||||
onClick={onNewClick}
|
onClick={onNewClick}
|
||||||
isNarrow={isPanelCollapsed}
|
isNarrow={isPanelCollapsed}
|
||||||
/>
|
/>
|
||||||
|
{ betaDot }
|
||||||
|
|
||||||
{ contextMenu }
|
{ contextMenu }
|
||||||
</li>;
|
</li>;
|
||||||
|
|
|
@ -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 {
|
||||||
|
if (window.electron?.getDesktopCapturerSources) {
|
||||||
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
|
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
|
||||||
const [source] = await finished;
|
const [source] = await finished;
|
||||||
isScreensharing = await this.props.call.setScreensharingEnabled(true, source);
|
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);
|
||||||
|
|
|
@ -799,15 +799,6 @@
|
||||||
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
|
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
|
||||||
"Change notification settings": "Change notification settings",
|
"Change notification settings": "Change notification settings",
|
||||||
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators",
|
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators",
|
||||||
"Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.",
|
|
||||||
"Spaces": "Spaces",
|
|
||||||
"Spaces are a new way to group rooms and people.": "Spaces are a new way to group rooms and people.",
|
|
||||||
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.",
|
|
||||||
"Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta available for web, desktop and Android. Thank you for trying the beta.",
|
|
||||||
"%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.",
|
|
||||||
"You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.",
|
|
||||||
"Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.",
|
|
||||||
"Your feedback will help make spaces better. The more detail you can go into, the better.": "Your feedback will help make spaces better. The more detail you can go into, the better.",
|
|
||||||
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
|
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
|
||||||
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
|
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
|
||||||
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
|
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
|
||||||
|
@ -883,6 +874,8 @@
|
||||||
"Show chat effects (animations when receiving e.g. confetti)": "Show chat effects (animations when receiving e.g. confetti)",
|
"Show chat effects (animations when receiving e.g. confetti)": "Show chat effects (animations when receiving e.g. confetti)",
|
||||||
"Show all rooms in Home": "Show all rooms in Home",
|
"Show all rooms in Home": "Show all rooms in Home",
|
||||||
"All rooms you're in will appear in Home.": "All rooms you're in will appear in Home.",
|
"All rooms you're in will appear in Home.": "All rooms you're in will appear in Home.",
|
||||||
|
"Display Communities instead of Spaces": "Display Communities instead of Spaces",
|
||||||
|
"Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.",
|
||||||
"Collecting app version information": "Collecting app version information",
|
"Collecting app version information": "Collecting app version information",
|
||||||
"Collecting logs": "Collecting logs",
|
"Collecting logs": "Collecting logs",
|
||||||
"Uploading logs": "Uploading logs",
|
"Uploading logs": "Uploading logs",
|
||||||
|
@ -1033,14 +1026,15 @@
|
||||||
"e.g. my-space": "e.g. my-space",
|
"e.g. my-space": "e.g. my-space",
|
||||||
"Address": "Address",
|
"Address": "Address",
|
||||||
"Create a space": "Create a space",
|
"Create a space": "Create a space",
|
||||||
|
"Spaces are a new way to group rooms and people.": "Spaces are a new way to group rooms and people.",
|
||||||
"What kind of Space do you want to create?": "What kind of Space do you want to create?",
|
"What kind of Space do you want to create?": "What kind of Space do you want to create?",
|
||||||
"You can change this later.": "You can change this later.",
|
"You can change this later.": "You can change this later.",
|
||||||
"Public": "Public",
|
"Public": "Public",
|
||||||
"Open space for anyone, best for communities": "Open space for anyone, best for communities",
|
"Open space for anyone, best for communities": "Open space for anyone, best for communities",
|
||||||
"Private": "Private",
|
"Private": "Private",
|
||||||
"Invite only, best for yourself or teams": "Invite only, best for yourself or teams",
|
"Invite only, best for yourself or teams": "Invite only, best for yourself or teams",
|
||||||
"You can also create a Space from a <a>community</a>.": "You can also create a Space from a <a>community</a>.",
|
"You can also make Spaces from <a>communities</a>.": "You can also make Spaces from <a>communities</a>.",
|
||||||
"To join an existing space you'll need an invite.": "To join an existing space you'll need an invite.",
|
"To join a space you'll need an invite.": "To join a space you'll need an invite.",
|
||||||
"Go back": "Go back",
|
"Go back": "Go back",
|
||||||
"Your public space": "Your public space",
|
"Your public space": "Your public space",
|
||||||
"Your private space": "Your private space",
|
"Your private space": "Your private space",
|
||||||
|
@ -1052,6 +1046,7 @@
|
||||||
"Show all rooms": "Show all rooms",
|
"Show all rooms": "Show all rooms",
|
||||||
"All rooms": "All rooms",
|
"All rooms": "All rooms",
|
||||||
"Options": "Options",
|
"Options": "Options",
|
||||||
|
"Spaces": "Spaces",
|
||||||
"Expand space panel": "Expand space panel",
|
"Expand space panel": "Expand space panel",
|
||||||
"Collapse space panel": "Collapse space panel",
|
"Collapse space panel": "Collapse space panel",
|
||||||
"Click to copy": "Click to copy",
|
"Click to copy": "Click to copy",
|
||||||
|
@ -1162,6 +1157,9 @@
|
||||||
"Anyone in a space can find and join. You can select multiple spaces.": "Anyone in a space can find and join. You can select multiple spaces.",
|
"Anyone in a space can find and join. You can select multiple spaces.": "Anyone in a space can find and join. You can select multiple spaces.",
|
||||||
"Space members": "Space members",
|
"Space members": "Space members",
|
||||||
"This upgrade will allow members of selected spaces access to this room without an invite.": "This upgrade will allow members of selected spaces access to this room without an invite.",
|
"This upgrade will allow members of selected spaces access to this room without an invite.": "This upgrade will allow members of selected spaces access to this room without an invite.",
|
||||||
|
"Before you upgrade": "Before you upgrade",
|
||||||
|
"This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.",
|
||||||
|
"Upgrade anyway": "Upgrade anyway",
|
||||||
"Message layout": "Message layout",
|
"Message layout": "Message layout",
|
||||||
"IRC": "IRC",
|
"IRC": "IRC",
|
||||||
"Modern": "Modern",
|
"Modern": "Modern",
|
||||||
|
@ -1662,6 +1660,7 @@
|
||||||
"%(count)s results|other": "%(count)s results",
|
"%(count)s results|other": "%(count)s results",
|
||||||
"%(count)s results|one": "%(count)s result",
|
"%(count)s results|one": "%(count)s result",
|
||||||
"This room": "This room",
|
"This room": "This room",
|
||||||
|
"Joining space …": "Joining space …",
|
||||||
"Joining room …": "Joining room …",
|
"Joining room …": "Joining room …",
|
||||||
"Loading …": "Loading …",
|
"Loading …": "Loading …",
|
||||||
"Rejecting invite …": "Rejecting invite …",
|
"Rejecting invite …": "Rejecting invite …",
|
||||||
|
@ -1839,7 +1838,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.",
|
||||||
|
@ -1978,7 +1977,7 @@
|
||||||
"Add reaction": "Add reaction",
|
"Add reaction": "Add reaction",
|
||||||
"Show all": "Show all",
|
"Show all": "Show all",
|
||||||
"Reactions": "Reactions",
|
"Reactions": "Reactions",
|
||||||
"<reactors/><reactedWith> reacted with %(content)s</reactedWith>": "<reactors/><reactedWith> reacted with %(content)s</reactedWith>",
|
"%(reactors)s reacted with %(content)s": "%(reactors)s reacted with %(content)s",
|
||||||
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
|
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
|
||||||
"Message deleted": "Message deleted",
|
"Message deleted": "Message deleted",
|
||||||
"Message deleted by %(name)s": "Message deleted by %(name)s",
|
"Message deleted by %(name)s": "Message deleted by %(name)s",
|
||||||
|
@ -2788,6 +2787,10 @@
|
||||||
"Create a Group Chat": "Create a Group Chat",
|
"Create a Group Chat": "Create a Group Chat",
|
||||||
"Upgrade to %(hostSignupBrand)s": "Upgrade to %(hostSignupBrand)s",
|
"Upgrade to %(hostSignupBrand)s": "Upgrade to %(hostSignupBrand)s",
|
||||||
"Open dial pad": "Open dial pad",
|
"Open dial pad": "Open dial pad",
|
||||||
|
"Public community": "Public community",
|
||||||
|
"Private community": "Private community",
|
||||||
|
"To view %(communityName)s, swap to communities in your <a>preferences</a>": "To view %(communityName)s, swap to communities in your <a>preferences</a>",
|
||||||
|
"To join %(communityName)s, swap to communities in your <a>preferences</a>": "To join %(communityName)s, swap to communities in your <a>preferences</a>",
|
||||||
"Failed to reject invitation": "Failed to reject invitation",
|
"Failed to reject invitation": "Failed to reject invitation",
|
||||||
"Cannot create rooms in this community": "Cannot create rooms in this community",
|
"Cannot create rooms in this community": "Cannot create rooms in this community",
|
||||||
"You do not have permission to create rooms in this community.": "You do not have permission to create rooms in this community.",
|
"You do not have permission to create rooms in this community.": "You do not have permission to create rooms in this community.",
|
||||||
|
@ -2818,7 +2821,6 @@
|
||||||
"Error whilst fetching joined communities": "Error whilst fetching joined communities",
|
"Error whilst fetching joined communities": "Error whilst fetching joined communities",
|
||||||
"Create a new community": "Create a new community",
|
"Create a new community": "Create a new community",
|
||||||
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
|
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
|
||||||
"Communities are changing to Spaces": "Communities are changing to Spaces",
|
|
||||||
"You’re all caught up": "You’re all caught up",
|
"You’re all caught up": "You’re all caught up",
|
||||||
"You have no visible notifications.": "You have no visible notifications.",
|
"You have no visible notifications.": "You have no visible notifications.",
|
||||||
"%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.",
|
"%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.",
|
||||||
|
@ -2885,8 +2887,8 @@
|
||||||
"Search names and descriptions": "Search names and descriptions",
|
"Search names and descriptions": "Search names and descriptions",
|
||||||
"Private space": "Private space",
|
"Private space": "Private space",
|
||||||
"<inviter/> invites you": "<inviter/> invites you",
|
"<inviter/> invites you": "<inviter/> invites you",
|
||||||
"To view %(spaceName)s, turn on the <a>Spaces beta</a>": "To view %(spaceName)s, turn on the <a>Spaces beta</a>",
|
"To view this Space, hide communities in your <a>preferences</a>": "To view this Space, hide communities in your <a>preferences</a>",
|
||||||
"To join %(spaceName)s, turn on the <a>Spaces beta</a>": "To join %(spaceName)s, turn on the <a>Spaces beta</a>",
|
"To join this Space, hide communities in your <a>preferences</a>": "To join this Space, hide communities in your <a>preferences</a>",
|
||||||
"To view %(spaceName)s, you need an invite": "To view %(spaceName)s, you need an invite",
|
"To view %(spaceName)s, you need an invite": "To view %(spaceName)s, you need an invite",
|
||||||
"Created from <Community />": "Created from <Community />",
|
"Created from <Community />": "Created from <Community />",
|
||||||
"Welcome to <name/>": "Welcome to <name/>",
|
"Welcome to <name/>": "Welcome to <name/>",
|
||||||
|
|
|
@ -16,9 +16,9 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
import React, { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
import { _t, _td } from '../languageHandler';
|
import { _td } from '../languageHandler';
|
||||||
import {
|
import {
|
||||||
NotificationBodyEnabledController,
|
NotificationBodyEnabledController,
|
||||||
NotificationsEnabledController,
|
NotificationsEnabledController,
|
||||||
|
@ -40,7 +40,6 @@ import { OrderedMultiController } from "./controllers/OrderedMultiController";
|
||||||
import { Layout } from "./Layout";
|
import { Layout } from "./Layout";
|
||||||
import ReducedMotionController from './controllers/ReducedMotionController';
|
import ReducedMotionController from './controllers/ReducedMotionController';
|
||||||
import IncompatibleController from "./controllers/IncompatibleController";
|
import IncompatibleController from "./controllers/IncompatibleController";
|
||||||
import SdkConfig from "../SdkConfig";
|
|
||||||
import PseudonymousAnalyticsController from './controllers/PseudonymousAnalyticsController';
|
import PseudonymousAnalyticsController from './controllers/PseudonymousAnalyticsController';
|
||||||
import NewLayoutSwitcherController from './controllers/NewLayoutSwitcherController';
|
import NewLayoutSwitcherController from './controllers/NewLayoutSwitcherController';
|
||||||
|
|
||||||
|
@ -145,44 +144,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
"feature_spaces": {
|
|
||||||
isFeature: true,
|
|
||||||
displayName: _td("Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. " +
|
|
||||||
"Requires compatible homeserver for some features."),
|
|
||||||
supportedLevels: LEVELS_FEATURE,
|
|
||||||
default: false,
|
|
||||||
controller: new ReloadOnChangeController(),
|
|
||||||
betaInfo: {
|
|
||||||
title: _td("Spaces"),
|
|
||||||
caption: _td("Spaces are a new way to group rooms and people."),
|
|
||||||
disclaimer: (enabled) => {
|
|
||||||
if (enabled) {
|
|
||||||
return <>
|
|
||||||
<p>{ _t("If you leave, %(brand)s will reload with Spaces disabled. " +
|
|
||||||
"Communities and custom tags will be visible again.", {
|
|
||||||
brand: SdkConfig.get().brand,
|
|
||||||
}) }</p>
|
|
||||||
<p>{ _t("Beta available for web, desktop and Android. Thank you for trying the beta.") }</p>
|
|
||||||
</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<p>{ _t("%(brand)s will reload with Spaces enabled. " +
|
|
||||||
"Communities and custom tags will be hidden.", {
|
|
||||||
brand: SdkConfig.get().brand,
|
|
||||||
}) }</p>
|
|
||||||
<b>{ _t("You can leave the beta any time from settings or tapping on a beta badge, " +
|
|
||||||
"like the one above.") }</b>
|
|
||||||
<p>{ _t("Beta available for web, desktop and Android. " +
|
|
||||||
"Some features may be unavailable on your homeserver.") }</p>
|
|
||||||
</>;
|
|
||||||
},
|
|
||||||
image: require("../../res/img/betas/spaces.png"),
|
|
||||||
feedbackSubheading: _td("Your feedback will help make spaces better. " +
|
|
||||||
"The more detail you can go into, the better."),
|
|
||||||
feedbackLabel: "spaces-feedback",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"feature_dnd": {
|
"feature_dnd": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
displayName: _td("Show options to enable 'Do not disturb' mode"),
|
displayName: _td("Show options to enable 'Do not disturb' mode"),
|
||||||
|
@ -203,7 +164,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
),
|
),
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
default: false,
|
default: false,
|
||||||
controller: new IncompatibleController("feature_spaces"),
|
controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", false, false),
|
||||||
},
|
},
|
||||||
"feature_pinning": {
|
"feature_pinning": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
|
@ -232,7 +193,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
displayName: _td("Group & filter rooms by custom tags (refresh to apply changes)"),
|
displayName: _td("Group & filter rooms by custom tags (refresh to apply changes)"),
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
default: false,
|
default: false,
|
||||||
controller: new IncompatibleController("feature_spaces"),
|
controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", false, false),
|
||||||
},
|
},
|
||||||
"feature_state_counters": {
|
"feature_state_counters": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
|
@ -780,6 +741,15 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
description: _td("All rooms you're in will appear in Home."),
|
description: _td("All rooms you're in will appear in Home."),
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
default: false,
|
default: false,
|
||||||
|
controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", null),
|
||||||
|
},
|
||||||
|
"showCommunitiesInsteadOfSpaces": {
|
||||||
|
displayName: _td("Display Communities instead of Spaces"),
|
||||||
|
description: _td("Temporarily show communities instead of Spaces for this session. " +
|
||||||
|
"Support for this will be removed in the near future. This will reload Element."),
|
||||||
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||||
|
default: false,
|
||||||
|
controller: new ReloadOnChangeController(),
|
||||||
},
|
},
|
||||||
[UIFeature.RoomHistorySettings]: {
|
[UIFeature.RoomHistorySettings]: {
|
||||||
supportedLevels: LEVELS_UI_FEATURE,
|
supportedLevels: LEVELS_UI_FEATURE,
|
||||||
|
@ -844,7 +814,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
[UIFeature.Communities]: {
|
[UIFeature.Communities]: {
|
||||||
supportedLevels: LEVELS_UI_FEATURE,
|
supportedLevels: LEVELS_UI_FEATURE,
|
||||||
default: true,
|
default: true,
|
||||||
controller: new IncompatibleController("feature_spaces"),
|
controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", false, false),
|
||||||
},
|
},
|
||||||
[UIFeature.AdvancedSettings]: {
|
[UIFeature.AdvancedSettings]: {
|
||||||
supportedLevels: LEVELS_UI_FEATURE,
|
supportedLevels: LEVELS_UI_FEATURE,
|
||||||
|
|
|
@ -467,6 +467,10 @@ export default class SettingsStore {
|
||||||
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
|
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!SettingsStore.isEnabled(settingName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// When non-beta features are specified in the config.json, we force them as enabled or disabled.
|
// When non-beta features are specified in the config.json, we force them as enabled or disabled.
|
||||||
if (SettingsStore.isFeature(settingName) && !SETTINGS[settingName]?.betaInfo) {
|
if (SettingsStore.isFeature(settingName) && !SETTINGS[settingName]?.betaInfo) {
|
||||||
const configVal = SettingsStore.getValueAt(SettingLevel.CONFIG, settingName, roomId, true, true);
|
const configVal = SettingsStore.getValueAt(SettingLevel.CONFIG, settingName, roomId, true, true);
|
||||||
|
|
|
@ -24,7 +24,11 @@ import SettingsStore from "../SettingsStore";
|
||||||
* labs flags.
|
* labs flags.
|
||||||
*/
|
*/
|
||||||
export default class IncompatibleController extends SettingController {
|
export default class IncompatibleController extends SettingController {
|
||||||
public constructor(private settingName: string, private forcedValue = false) {
|
public constructor(
|
||||||
|
private settingName: string,
|
||||||
|
private forcedValue: any = false,
|
||||||
|
private incompatibleValue: any = true,
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,13 +38,17 @@ export default class IncompatibleController extends SettingController {
|
||||||
calculatedValue: any,
|
calculatedValue: any,
|
||||||
calculatedAtLevel: SettingLevel,
|
calculatedAtLevel: SettingLevel,
|
||||||
): any {
|
): any {
|
||||||
if (this.incompatibleSettingEnabled) {
|
if (this.incompatibleSetting) {
|
||||||
return this.forcedValue;
|
return this.forcedValue;
|
||||||
}
|
}
|
||||||
return null; // no override
|
return null; // no override
|
||||||
}
|
}
|
||||||
|
|
||||||
public get incompatibleSettingEnabled(): boolean {
|
public get settingDisabled(): boolean {
|
||||||
return SettingsStore.getValue(this.settingName);
|
return this.incompatibleSetting;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get incompatibleSetting(): boolean {
|
||||||
|
return SettingsStore.getValue(this.settingName) === this.incompatibleValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,7 @@ export interface ISuggestedRoom extends IHierarchyRoom {
|
||||||
const MAX_SUGGESTED_ROOMS = 20;
|
const MAX_SUGGESTED_ROOMS = 20;
|
||||||
|
|
||||||
// This setting causes the page to reload and can be costly if read frequently, so read it here only
|
// This setting causes the page to reload and can be costly if read frequently, so read it here only
|
||||||
const spacesEnabled = SettingsStore.getValue("feature_spaces");
|
const spacesEnabled = !SettingsStore.getValue("showCommunitiesInsteadOfSpaces");
|
||||||
|
|
||||||
const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "HOME_SPACE"}`;
|
const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "HOME_SPACE"}`;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import { base32 } from "rfc4648";
|
import { base32 } from "rfc4648";
|
||||||
|
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
// Dev note: the interface is split in two so we don't have to disable the
|
// Dev note: the interface is split in two so we don't have to disable the
|
||||||
// linter across the whole project.
|
// linter across the whole project.
|
||||||
|
@ -53,6 +54,9 @@ export interface IOOBData {
|
||||||
name?: string; // The room's name
|
name?: string; // The room's name
|
||||||
avatarUrl?: string; // The mxc:// avatar URL for the room
|
avatarUrl?: string; // The mxc:// avatar URL for the room
|
||||||
inviterName?: string; // The display name of the person who invited us to the room
|
inviterName?: string; // The display name of the person who invited us to the room
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
room_name?: string; // The name of the room, to be used until we are told better by the server
|
||||||
|
roomType?: RoomType; // The type of the room, to be used until we are told better by the server
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_PREFIX = "mx_threepid_invite_";
|
const STORAGE_PREFIX = "mx_threepid_invite_";
|
||||||
|
|
|
@ -104,7 +104,10 @@ export function getUserNameColorClass(userId: string): string {
|
||||||
* @returns {string} a string constructed by joining `items` with a comma
|
* @returns {string} a string constructed by joining `items` with a comma
|
||||||
* between each item, but with the last item appended as " and [lastItem]".
|
* between each item, but with the last item appended as " and [lastItem]".
|
||||||
*/
|
*/
|
||||||
export function formatCommaSeparatedList(items: Array<string | JSX.Element>, itemLimit?: number): string | JSX.Element {
|
export function formatCommaSeparatedList(items: string[], itemLimit?: number): string;
|
||||||
|
export function formatCommaSeparatedList(items: JSX.Element[], itemLimit?: number): JSX.Element;
|
||||||
|
export function formatCommaSeparatedList(items: Array<JSX.Element | string>, itemLimit?: number): JSX.Element | string;
|
||||||
|
export function formatCommaSeparatedList(items: Array<JSX.Element | string>, itemLimit?: number): JSX.Element | string {
|
||||||
const remaining = itemLimit === undefined ? 0 : Math.max(
|
const remaining = itemLimit === undefined ? 0 : Math.max(
|
||||||
items.length - itemLimit, 0,
|
items.length - itemLimit, 0,
|
||||||
);
|
);
|
||||||
|
@ -112,11 +115,25 @@ export function formatCommaSeparatedList(items: Array<string | JSX.Element>, ite
|
||||||
return "";
|
return "";
|
||||||
} else if (items.length === 1) {
|
} else if (items.length === 1) {
|
||||||
return items[0];
|
return items[0];
|
||||||
} else if (remaining > 0) {
|
|
||||||
items = items.slice(0, itemLimit);
|
|
||||||
return _t("%(items)s and %(count)s others", { items: jsxJoin(items, ', '), count: remaining } );
|
|
||||||
} else {
|
} else {
|
||||||
const lastItem = items.pop();
|
let lastItem;
|
||||||
return _t("%(items)s and %(lastItem)s", { items: jsxJoin(items, ', '), lastItem: lastItem });
|
if (remaining > 0) {
|
||||||
|
items = items.slice(0, itemLimit);
|
||||||
|
} else {
|
||||||
|
lastItem = items.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
let joinedItems;
|
||||||
|
if (items.every(e => typeof e === "string")) {
|
||||||
|
joinedItems = items.join(", ");
|
||||||
|
} else {
|
||||||
|
joinedItems = jsxJoin(items, ", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining > 0) {
|
||||||
|
return _t("%(items)s and %(count)s others", { items: joinedItems, count: remaining } );
|
||||||
|
} else {
|
||||||
|
return _t("%(items)s and %(lastItem)s", { items: joinedItems, lastItem });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,23 +9,16 @@ import sdk from '../../../skinned-sdk';
|
||||||
|
|
||||||
import dis from '../../../../src/dispatcher/dispatcher';
|
import dis from '../../../../src/dispatcher/dispatcher';
|
||||||
import DMRoomMap from '../../../../src/utils/DMRoomMap';
|
import DMRoomMap from '../../../../src/utils/DMRoomMap';
|
||||||
import GroupStore from '../../../../src/stores/GroupStore';
|
|
||||||
|
|
||||||
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
|
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
|
||||||
import { DefaultTagID } from "../../../../src/stores/room-list/models";
|
import { DefaultTagID } from "../../../../src/stores/room-list/models";
|
||||||
import RoomListStore, { LISTS_UPDATE_EVENT, RoomListStoreClass } from "../../../../src/stores/room-list/RoomListStore";
|
import RoomListStore, { RoomListStoreClass } from "../../../../src/stores/room-list/RoomListStore";
|
||||||
import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore";
|
import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore";
|
||||||
|
|
||||||
function generateRoomId() {
|
function generateRoomId() {
|
||||||
return '!' + Math.random().toString().slice(2, 10) + ':domain';
|
return '!' + Math.random().toString().slice(2, 10) + ':domain';
|
||||||
}
|
}
|
||||||
|
|
||||||
function waitForRoomListStoreUpdate() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
RoomListStore.instance.once(LISTS_UPDATE_EVENT, () => resolve());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('RoomList', () => {
|
describe('RoomList', () => {
|
||||||
function createRoom(opts) {
|
function createRoom(opts) {
|
||||||
const room = new Room(generateRoomId(), MatrixClientPeg.get(), client.getUserId(), {
|
const room = new Room(generateRoomId(), MatrixClientPeg.get(), client.getUserId(), {
|
||||||
|
@ -239,73 +232,6 @@ describe('RoomList', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('when no tags are selected', () => {
|
|
||||||
itDoesCorrectOptimisticUpdatesForDraggedRoomTiles();
|
itDoesCorrectOptimisticUpdatesForDraggedRoomTiles();
|
||||||
});
|
|
||||||
|
|
||||||
describe('when tags are selected', () => {
|
|
||||||
function setupSelectedTag() {
|
|
||||||
// Simulate a complete sync BEFORE dispatching anything else
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'MatrixActions.sync',
|
|
||||||
prevState: null,
|
|
||||||
state: 'PREPARED',
|
|
||||||
matrixClient: client,
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
// Simulate joined groups being received
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'GroupActions.fetchJoinedGroups.success',
|
|
||||||
result: {
|
|
||||||
groups: ['+group:domain'],
|
|
||||||
},
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
// Simulate receiving tag ordering account data
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'MatrixActions.accountData',
|
|
||||||
event_type: 'im.vector.web.tag_ordering',
|
|
||||||
event_content: {
|
|
||||||
tags: ['+group:domain'],
|
|
||||||
},
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
// GroupStore is not flux, mock and notify
|
|
||||||
GroupStore.getGroupRooms = (groupId) => {
|
|
||||||
return [movingRoom];
|
|
||||||
};
|
|
||||||
GroupStore._notifyListeners();
|
|
||||||
|
|
||||||
// We also have to mock the client's getGroup function for the room list to filter it.
|
|
||||||
// It's not smart enough to tell the difference between a real group and a template though.
|
|
||||||
client.getGroup = (groupId) => {
|
|
||||||
return { groupId };
|
|
||||||
};
|
|
||||||
|
|
||||||
// Select tag
|
|
||||||
dis.dispatch({ action: 'select_tag', tag: '+group:domain' }, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
setupSelectedTag();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays the correct rooms when the groups rooms are changed', async () => {
|
|
||||||
GroupStore.getGroupRooms = (groupId) => {
|
|
||||||
return [movingRoom, otherRoom];
|
|
||||||
};
|
|
||||||
GroupStore._notifyListeners();
|
|
||||||
|
|
||||||
await waitForRoomListStoreUpdate();
|
|
||||||
|
|
||||||
// XXX: Even though the store updated, it can take a bit before the update makes
|
|
||||||
// it to the components. This gives it plenty of time to figure out what to do.
|
|
||||||
await (new Promise(resolve => setTimeout(resolve, 500)));
|
|
||||||
|
|
||||||
expectRoomInSubList(otherRoom, (s) => s.props.tagId === DefaultTagID.Untagged);
|
|
||||||
});
|
|
||||||
|
|
||||||
itDoesCorrectOptimisticUpdatesForDraggedRoomTiles();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,9 @@ module.exports = async function scenario(createSession, restCreator) {
|
||||||
console.log("create REST users:");
|
console.log("create REST users:");
|
||||||
const charlies = await createRestUsers(restCreator);
|
const charlies = await createRestUsers(restCreator);
|
||||||
await lazyLoadingScenarios(alice, bob, charlies);
|
await lazyLoadingScenarios(alice, bob, charlies);
|
||||||
|
// do spaces scenarios last as the rest of the tests may get confused by spaces
|
||||||
|
// XXX: disabled for now as fails in CI but succeeds locally
|
||||||
|
// await spacesScenarios(alice, bob);
|
||||||
};
|
};
|
||||||
|
|
||||||
async function createRestUsers(restCreator) {
|
async function createRestUsers(restCreator) {
|
||||||
|
|
32
test/end-to-end-tests/src/scenarios/spaces.js
Normal file
32
test/end-to-end-tests/src/scenarios/spaces.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { createSpace, inviteSpace } = require("../usecases/create-space");
|
||||||
|
|
||||||
|
module.exports = async function spacesScenarios(alice, bob) {
|
||||||
|
console.log(" creating a space for spaces scenarios:");
|
||||||
|
|
||||||
|
await alice.delay(1000); // wait for dialogs to close
|
||||||
|
await setupSpaceUsingAliceAndInviteBob(alice, bob);
|
||||||
|
};
|
||||||
|
|
||||||
|
const space = "Test Space";
|
||||||
|
|
||||||
|
async function setupSpaceUsingAliceAndInviteBob(alice, bob) {
|
||||||
|
await createSpace(alice, space);
|
||||||
|
await inviteSpace(alice, space, "@bob:localhost");
|
||||||
|
await bob.query(`.mx_SpaceButton[aria-label="${space}"]`); // assert invite received
|
||||||
|
}
|
80
test/end-to-end-tests/src/usecases/create-space.js
Normal file
80
test/end-to-end-tests/src/usecases/create-space.js
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function openSpaceCreateMenu(session) {
|
||||||
|
const spaceCreateButton = await session.query('.mx_SpaceButton_new');
|
||||||
|
await spaceCreateButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSpace(session, name, isPublic = false) {
|
||||||
|
session.log.step(`creates space "${name}"`);
|
||||||
|
|
||||||
|
await openSpaceCreateMenu(session);
|
||||||
|
const className = isPublic ? ".mx_SpaceCreateMenuType_public" : ".mx_SpaceCreateMenuType_private";
|
||||||
|
const visibilityButton = await session.query(className);
|
||||||
|
await visibilityButton.click();
|
||||||
|
|
||||||
|
const nameInput = await session.query('input[name="spaceName"]');
|
||||||
|
await session.replaceInputText(nameInput, name);
|
||||||
|
|
||||||
|
await session.delay(100);
|
||||||
|
|
||||||
|
const createButton = await session.query('.mx_SpaceCreateMenu_wrapper .mx_AccessibleButton_kind_primary');
|
||||||
|
await createButton.click();
|
||||||
|
|
||||||
|
if (!isPublic) {
|
||||||
|
const justMeButton = await session.query('.mx_SpaceRoomView_privateScope_justMeButton');
|
||||||
|
await justMeButton.click();
|
||||||
|
const continueButton = await session.query('.mx_AddExistingToSpace_footer .mx_AccessibleButton_kind_primary');
|
||||||
|
await continueButton.click();
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
const continueButton = await session.query('.mx_SpaceRoomView_buttons .mx_AccessibleButton_kind_primary');
|
||||||
|
await continueButton.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session.log.done();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inviteSpace(session, spaceName, userId) {
|
||||||
|
session.log.step(`invites "${userId}" to space "${spaceName}"`);
|
||||||
|
|
||||||
|
const spaceButton = await session.query(`.mx_SpaceButton[aria-label="${spaceName}"]`);
|
||||||
|
await spaceButton.click({
|
||||||
|
button: 'right',
|
||||||
|
});
|
||||||
|
|
||||||
|
const inviteButton = await session.query('[aria-label="Invite people"]');
|
||||||
|
await inviteButton.click();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const button = await session.query('.mx_SpacePublicShare_inviteButton');
|
||||||
|
await button.click();
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
const inviteTextArea = await session.query(".mx_InviteDialog_editor input");
|
||||||
|
await inviteTextArea.type(userId);
|
||||||
|
const selectUserItem = await session.query(".mx_InviteDialog_roomTile");
|
||||||
|
await selectUserItem.click();
|
||||||
|
const confirmButton = await session.query(".mx_InviteDialog_goButton");
|
||||||
|
await confirmButton.click();
|
||||||
|
session.log.done();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { openSpaceCreateMenu, createSpace, inviteSpace };
|
|
@ -161,8 +161,8 @@ async function changeRoomSettings(session, settings) {
|
||||||
if (settings.visibility) {
|
if (settings.visibility) {
|
||||||
session.log.step(`sets visibility to ${settings.visibility}`);
|
session.log.step(`sets visibility to ${settings.visibility}`);
|
||||||
const radios = await session.queryAll(".mx_RoomSettingsDialog label");
|
const radios = await session.queryAll(".mx_RoomSettingsDialog label");
|
||||||
assert.equal(radios.length, 6);
|
assert.equal(radios.length, 7);
|
||||||
const [inviteOnlyRoom, publicRoom] = radios;
|
const [inviteOnlyRoom,, publicRoom] = radios;
|
||||||
|
|
||||||
if (settings.visibility === "invite_only") {
|
if (settings.visibility === "invite_only") {
|
||||||
await inviteOnlyRoom.click();
|
await inviteOnlyRoom.click();
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
/*
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// This needs to be executed before the SpaceStore gets imported but due to ES6 import hoisting we have to do this here.
|
|
||||||
// SpaceStore reads the SettingsStore which needs the localStorage values set at init time.
|
|
||||||
|
|
||||||
localStorage.setItem("mx_labs_feature_feature_spaces", "true");
|
|
|
@ -18,7 +18,6 @@ import { EventEmitter } from "events";
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
|
||||||
import "./SpaceStore-setup"; // enable space lab
|
|
||||||
import "../skinned-sdk"; // Must be first for skinning to work
|
import "../skinned-sdk"; // Must be first for skinning to work
|
||||||
import SpaceStore, {
|
import SpaceStore, {
|
||||||
UPDATE_HOME_BEHAVIOUR,
|
UPDATE_HOME_BEHAVIOUR,
|
||||||
|
|
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "../SpaceStore-setup"; // enable space lab
|
|
||||||
import "../../skinned-sdk"; // Must be first for skinning to work
|
import "../../skinned-sdk"; // Must be first for skinning to work
|
||||||
import { SpaceWatcher } from "../../../src/stores/room-list/SpaceWatcher";
|
import { SpaceWatcher } from "../../../src/stores/room-list/SpaceWatcher";
|
||||||
import type { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore";
|
import type { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore";
|
||||||
|
|
Loading…
Reference in a new issue