Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/18969

 Conflicts:
	src/components/views/dialogs/LeaveSpaceDialog.tsx
	src/i18n/strings/en_EN.json
This commit is contained in:
Michael Telatynski 2021-09-27 11:18:57 +01:00
commit 6d0af83df4
236 changed files with 4971 additions and 17999 deletions

View file

@ -5,6 +5,8 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
PR_NUMBER: ${{github.event.number}}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Build - name: Build

View file

@ -5,6 +5,8 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
PR_NUMBER: ${{github.event.number}}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: c-hive/gha-yarn-cache@v2 - uses: c-hive/gha-yarn-cache@v2

View file

@ -34,18 +34,43 @@ limitations under the License.
transition: opacity 300ms ease; transition: opacity 300ms ease;
} }
@keyframes mx--anim-pulse { @keyframes mx--anim-pulse {
0% { opacity: 1; } 0% { opacity: 1; }
50% { opacity: 0.7; } 50% { opacity: 0.7; }
100% { opacity: 1; } 100% { opacity: 1; }
} }
@keyframes mx_Dialog_lightbox_background_keyframes {
from {
opacity: 0;
}
to {
opacity: $lightbox-background-bg-opacity;
}
}
@keyframes mx_ImageView_panel_keyframes {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (prefers-reduced-motion) { @media (prefers-reduced-motion) {
@keyframes mx--anim-pulse { @keyframes mx--anim-pulse {
// Override all keyframes in reduced-motion // Override all keyframes in reduced-motion
} }
@keyframes mx_Dialog_lightbox_background_keyframes {
// Override all keyframes in reduced-motion
}
@keyframes mx_ImageView_panel_keyframes {
// Override all keyframes in reduced-motion
}
.mx_rtg--fade-enter-active { .mx_rtg--fade-enter-active {
transition: none; transition: none;
} }

View file

@ -318,6 +318,8 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
.mx_Dialog_lightbox .mx_Dialog_background { .mx_Dialog_lightbox .mx_Dialog_background {
opacity: $lightbox-background-bg-opacity; opacity: $lightbox-background-bg-opacity;
background-color: $lightbox-background-bg-color; background-color: $lightbox-background-bg-color;
animation-name: mx_Dialog_lightbox_background_keyframes;
animation-duration: 300ms;
} }
.mx_Dialog_lightbox .mx_Dialog { .mx_Dialog_lightbox .mx_Dialog {

View file

@ -183,3 +183,40 @@ limitations under the License.
padding: 0; padding: 0;
} }
} }
@media screen and (max-width: 700px) {
.mx_RoomDirectory_roomMemberCount {
padding: 0px;
}
.mx_AccessibleButton_kind_secondary {
padding: 0px !important;
}
.mx_RoomDirectory_join {
margin-left: 0px;
}
.mx_RoomDirectory_alias {
margin-top: 10px;
margin-bottom: 10px;
}
.mx_RoomDirectory_roomDescription {
padding-bottom: 0px;
}
.mx_RoomDirectory_name {
margin-bottom: 5px;
}
.mx_RoomDirectory_roomAvatar {
margin-top: 10px;
}
.mx_RoomDirectory_table {
grid-template-columns: auto;
row-gap: 14px;
margin-top: 5px;
}
}

View file

@ -18,6 +18,10 @@ $button-size: 32px;
$icon-size: 22px; $icon-size: 22px;
$button-gap: 24px; $button-gap: 24px;
:root {
--image-view-panel-height: 68px;
}
.mx_ImageView { .mx_ImageView {
display: flex; display: flex;
width: 100%; width: 100%;
@ -36,14 +40,24 @@ $button-gap: 24px;
.mx_ImageView_image { .mx_ImageView_image {
flex-shrink: 0; flex-shrink: 0;
&.mx_ImageView_image_animating {
transition: transform 200ms ease 0s;
}
&.mx_ImageView_image_animatingLoading {
transition: transform 300ms ease 0s;
}
} }
.mx_ImageView_panel { .mx_ImageView_panel {
width: 100%; width: 100%;
height: 68px; height: var(--image-view-panel-height);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
animation-name: mx_ImageView_panel_keyframes;
animation-duration: 300ms;
} }
.mx_ImageView_info_wrapper { .mx_ImageView_info_wrapper {
@ -124,3 +138,13 @@ $button-gap: 24px;
mask-size: 40%; mask-size: 40%;
} }
} }
@media (prefers-reduced-motion) {
.mx_ImageView_image_animating {
transition: none !important;
}
.mx_ImageView_image_animatingLoading {
transition: none !important;
}
}

View file

@ -732,6 +732,11 @@ $hover-select-border: 4px;
margin-top: 0; margin-top: 0;
padding-bottom: 5px; padding-bottom: 5px;
margin-bottom: 5px; margin-bottom: 5px;
.mx_MessageTimestamp {
left: auto;
right: 0;
}
} }
.mx_MessageComposer_sendMessage { .mx_MessageComposer_sendMessage {

View file

@ -185,16 +185,26 @@ limitations under the License.
} }
} }
.mx_ContextualMenu {
.mx_MessageComposer_button {
padding-left: calc(var(--size) + 6px);
}
}
.mx_MessageComposer_button { .mx_MessageComposer_button {
--size: 26px; --size: 26px;
position: relative; position: relative;
margin-right: 6px;
cursor: pointer; cursor: pointer;
height: var(--size); height: var(--size);
line-height: var(--size); line-height: var(--size);
width: auto; width: auto;
padding-left: calc(var(--size) + 5px); padding-left: var(--size);
border-radius: 100%; border-radius: 100%;
margin-right: 6px;
&:last-child {
margin-right: auto;
}
&::before { &::before {
content: ''; content: '';

View file

@ -49,6 +49,8 @@ import PerformanceMonitor from "../performance";
import UIStore from "../stores/UIStore"; import UIStore from "../stores/UIStore";
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore"; import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
import { RoomScrollStateStore } from "../stores/RoomScrollStateStore"; import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake";
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
@ -92,6 +94,7 @@ declare global {
mxUIStore: UIStore; mxUIStore: UIStore;
mxSetupEncryptionStore?: SetupEncryptionStore; mxSetupEncryptionStore?: SetupEncryptionStore;
mxRoomScrollStateStore?: RoomScrollStateStore; mxRoomScrollStateStore?: RoomScrollStateStore;
mxActiveWidgetStore?: ActiveWidgetStore;
mxOnRecaptchaLoaded?: () => void; mxOnRecaptchaLoaded?: () => void;
electron?: Electron; electron?: Electron;
} }
@ -223,6 +226,15 @@ declare global {
) => string; ) => string;
isReady: () => boolean; isReady: () => boolean;
}; };
// eslint-disable-next-line no-var, camelcase
var mx_rage_logger: ConsoleLogger;
// eslint-disable-next-line no-var, camelcase
var mx_rage_initPromise: Promise<void>;
// eslint-disable-next-line no-var, camelcase
var mx_rage_initStoragePromise: Promise<void>;
// eslint-disable-next-line no-var, camelcase
var mx_rage_store: IndexedDBLogStore;
} }
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */

View file

@ -20,6 +20,8 @@ import * as sdk from './index';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import { IDialogProps } from "./components/views/dialogs/IDialogProps"; import { IDialogProps } from "./components/views/dialogs/IDialogProps";
import { logger } from "matrix-js-sdk/src/logger";
type AsyncImport<T> = { default: T }; type AsyncImport<T> = { default: T };
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
@ -47,7 +49,7 @@ export default class AsyncWrapper extends React.Component<IProps, IState> {
componentDidMount() { componentDidMount() {
// 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('Starting load of AsyncWrapper for modal'); logger.log('Starting load of AsyncWrapper for modal');
this.props.prom.then((result) => { this.props.prom.then((result) => {
if (this.unmounted) return; if (this.unmounted) return;

View file

@ -286,9 +286,9 @@ export default class CallHandler extends EventEmitter {
dis.dispatch({ action: Action.VirtualRoomSupportUpdated }); dis.dispatch({ action: Action.VirtualRoomSupportUpdated });
} catch (e) { } catch (e) {
if (maxTries === 1) { if (maxTries === 1) {
console.log("Failed to check for protocol support and no retries remain: assuming no support", e); logger.log("Failed to check for protocol support and no retries remain: assuming no support", e);
} else { } else {
console.log("Failed to check for protocol support: will retry", e); logger.log("Failed to check for protocol support: will retry", e);
this.pstnSupportCheckTimer = setTimeout(() => { this.pstnSupportCheckTimer = setTimeout(() => {
this.checkProtocols(maxTries - 1); this.checkProtocols(maxTries - 1);
}, 10000); }, 10000);
@ -399,7 +399,7 @@ export default class CallHandler extends EventEmitter {
// or chrome doesn't think so and is denying the request. Not sure what // or chrome doesn't think so and is denying the request. Not sure what
// we can really do here... // we can really do here...
// https://github.com/vector-im/element-web/issues/7657 // https://github.com/vector-im/element-web/issues/7657
console.log("Unable to play audio clip", e); logger.log("Unable to play audio clip", e);
} }
}; };
if (this.audioPromises.has(audioId)) { if (this.audioPromises.has(audioId)) {
@ -477,7 +477,7 @@ export default class CallHandler extends EventEmitter {
call.on(CallEvent.Replaced, (newCall: MatrixCall) => { call.on(CallEvent.Replaced, (newCall: MatrixCall) => {
if (!this.matchesCallForThisRoom(call)) return; if (!this.matchesCallForThisRoom(call)) return;
console.log(`Call ID ${call.callId} is being replaced by call ID ${newCall.callId}`); logger.log(`Call ID ${call.callId} is being replaced by call ID ${newCall.callId}`);
if (call.state === CallState.Ringing) { if (call.state === CallState.Ringing) {
this.pause(AudioID.Ring); this.pause(AudioID.Ring);
@ -493,7 +493,7 @@ export default class CallHandler extends EventEmitter {
call.on(CallEvent.AssertedIdentityChanged, async () => { call.on(CallEvent.AssertedIdentityChanged, async () => {
if (!this.matchesCallForThisRoom(call)) return; if (!this.matchesCallForThisRoom(call)) return;
console.log(`Call ID ${call.callId} got new asserted identity:`, call.getRemoteAssertedIdentity()); logger.log(`Call ID ${call.callId} got new asserted identity:`, call.getRemoteAssertedIdentity());
const newAssertedIdentity = call.getRemoteAssertedIdentity().id; const newAssertedIdentity = call.getRemoteAssertedIdentity().id;
let newNativeAssertedIdentity = newAssertedIdentity; let newNativeAssertedIdentity = newAssertedIdentity;
@ -503,7 +503,7 @@ export default class CallHandler extends EventEmitter {
newNativeAssertedIdentity = response[0].userid; newNativeAssertedIdentity = response[0].userid;
} }
} }
console.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`); logger.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`);
if (newNativeAssertedIdentity) { if (newNativeAssertedIdentity) {
this.assertedIdentityNativeUsers[call.callId] = newNativeAssertedIdentity; this.assertedIdentityNativeUsers[call.callId] = newNativeAssertedIdentity;
@ -516,11 +516,11 @@ export default class CallHandler extends EventEmitter {
await ensureDMExists(MatrixClientPeg.get(), newNativeAssertedIdentity); await ensureDMExists(MatrixClientPeg.get(), newNativeAssertedIdentity);
const newMappedRoomId = this.roomIdForCall(call); const newMappedRoomId = this.roomIdForCall(call);
console.log(`Old room ID: ${mappedRoomId}, new room ID: ${newMappedRoomId}`); logger.log(`Old room ID: ${mappedRoomId}, new room ID: ${newMappedRoomId}`);
if (newMappedRoomId !== mappedRoomId) { if (newMappedRoomId !== mappedRoomId) {
this.removeCallForRoom(mappedRoomId); this.removeCallForRoom(mappedRoomId);
mappedRoomId = newMappedRoomId; mappedRoomId = newMappedRoomId;
console.log("Moving call to room " + mappedRoomId); logger.log("Moving call to room " + mappedRoomId);
this.addCallForRoom(mappedRoomId, call, true); this.addCallForRoom(mappedRoomId, call, true);
} }
} }
@ -656,7 +656,7 @@ export default class CallHandler extends EventEmitter {
private setCallState(call: MatrixCall, status: CallState) { private setCallState(call: MatrixCall, status: CallState) {
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
console.log( logger.log(
`Call state in ${mappedRoomId} changed to ${status}`, `Call state in ${mappedRoomId} changed to ${status}`,
); );
@ -681,7 +681,7 @@ export default class CallHandler extends EventEmitter {
} }
private removeCallForRoom(roomId: string) { private removeCallForRoom(roomId: string) {
console.log("Removing call for room ", roomId); logger.log("Removing call for room ", roomId);
this.calls.delete(roomId); this.calls.delete(roomId);
this.emit(CallHandlerEvent.CallsChanged, this.calls); this.emit(CallHandlerEvent.CallsChanged, this.calls);
} }
@ -752,7 +752,7 @@ export default class CallHandler extends EventEmitter {
logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId); logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId);
const timeUntilTurnCresExpire = MatrixClientPeg.get().getTurnServersExpiry() - Date.now(); const timeUntilTurnCresExpire = MatrixClientPeg.get().getTurnServersExpiry() - Date.now();
console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms"); logger.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
const call = MatrixClientPeg.get().createCall(mappedRoomId); const call = MatrixClientPeg.get().createCall(mappedRoomId);
try { try {
@ -862,7 +862,7 @@ export default class CallHandler extends EventEmitter {
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
if (this.getCallForRoom(mappedRoomId)) { if (this.getCallForRoom(mappedRoomId)) {
console.log( logger.log(
"Got incoming call for room " + mappedRoomId + "Got incoming call for room " + mappedRoomId +
" but there's already a call for this room: ignoring", " but there's already a call for this room: ignoring",
); );
@ -966,7 +966,7 @@ export default class CallHandler extends EventEmitter {
const nativeLookupResults = await this.sipNativeLookup(userId); const nativeLookupResults = await this.sipNativeLookup(userId);
const lookupSuccess = nativeLookupResults.length > 0 && nativeLookupResults[0].fields.lookup_success; const lookupSuccess = nativeLookupResults.length > 0 && nativeLookupResults[0].fields.lookup_success;
nativeUserId = lookupSuccess ? nativeLookupResults[0].userid : userId; nativeUserId = lookupSuccess ? nativeLookupResults[0].userid : userId;
console.log("Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId); logger.log("Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId);
} else { } else {
nativeUserId = userId; nativeUserId = userId;
} }
@ -1014,7 +1014,7 @@ export default class CallHandler extends EventEmitter {
try { try {
await call.transfer(destination); await call.transfer(destination);
} catch (e) { } catch (e) {
console.log("Failed to transfer call", e); logger.log("Failed to transfer call", e);
Modal.createTrackedDialog('Failed to transfer call', '', ErrorDialog, { Modal.createTrackedDialog('Failed to transfer call', '', ErrorDialog, {
title: _t('Transfer Failed'), title: _t('Transfer Failed'),
description: _t('Failed to transfer call'), description: _t('Failed to transfer call'),
@ -1104,7 +1104,7 @@ export default class CallHandler extends EventEmitter {
); );
WidgetUtils.setRoomWidget(roomId, widgetId, WidgetType.JITSI, widgetUrl, 'Jitsi', widgetData).then(() => { WidgetUtils.setRoomWidget(roomId, widgetId, WidgetType.JITSI, widgetUrl, 'Jitsi', widgetData).then(() => {
console.log('Jitsi widget added'); logger.log('Jitsi widget added');
}).catch((e) => { }).catch((e) => {
if (e.errcode === 'M_FORBIDDEN') { if (e.errcode === 'M_FORBIDDEN') {
Modal.createTrackedDialog('Call Failed', '', ErrorDialog, { Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
@ -1152,11 +1152,11 @@ export default class CallHandler extends EventEmitter {
private addCallForRoom(roomId: string, call: MatrixCall, changedRooms = false): void { private addCallForRoom(roomId: string, call: MatrixCall, changedRooms = false): void {
if (this.calls.has(roomId)) { if (this.calls.has(roomId)) {
console.log(`Couldn't add call to room ${roomId}: already have a call for this room`); logger.log(`Couldn't add call to room ${roomId}: already have a call for this room`);
throw new Error("Already have a call for room " + roomId); throw new Error("Already have a call for room " + roomId);
} }
console.log("setting call for room " + roomId); logger.log("setting call for room " + roomId);
this.calls.set(roomId, call); this.calls.set(roomId, call);
// Should we always emit CallsChanged too? // Should we always emit CallsChanged too?

View file

@ -42,6 +42,8 @@ import { BlurhashEncoder } from "./BlurhashEncoder";
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
import { decorateStartSendingTime, sendRoundTripMetric } from "./sendTimePerformanceMetrics"; import { decorateStartSendingTime, sendRoundTripMetric } from "./sendTimePerformanceMetrics";
import { logger } from "matrix-js-sdk/src/logger";
const MAX_WIDTH = 800; const MAX_WIDTH = 800;
const MAX_HEIGHT = 600; const MAX_HEIGHT = 600;
@ -678,13 +680,13 @@ export default class ContentMessages {
private ensureMediaConfigFetched(matrixClient: MatrixClient) { private ensureMediaConfigFetched(matrixClient: MatrixClient) {
if (this.mediaConfig !== null) return; if (this.mediaConfig !== null) return;
console.log("[Media Config] Fetching"); logger.log("[Media Config] Fetching");
return matrixClient.getMediaConfig().then((config) => { return matrixClient.getMediaConfig().then((config) => {
console.log("[Media Config] Fetched config:", config); logger.log("[Media Config] Fetched config:", config);
return config; return config;
}).catch(() => { }).catch(() => {
// Media repo can't or won't report limits, so provide an empty object (no limits). // Media repo can't or won't report limits, so provide an empty object (no limits).
console.log("[Media Config] Could not fetch config, so not limiting uploads."); logger.log("[Media Config] Could not fetch config, so not limiting uploads.");
return {}; return {};
}).then((config) => { }).then((config) => {
this.mediaConfig = config; this.mediaConfig = config;

View file

@ -30,6 +30,8 @@ const HEARTBEAT_INTERVAL = 5_000; // ms
const SESSION_UPDATE_INTERVAL = 60; // seconds const SESSION_UPDATE_INTERVAL = 60; // seconds
const MAX_PENDING_EVENTS = 1000; const MAX_PENDING_EVENTS = 1000;
export type Rating = 1 | 2 | 3 | 4 | 5;
enum Orientation { enum Orientation {
Landscape = "landscape", Landscape = "landscape",
Portrait = "portrait", Portrait = "portrait",
@ -451,7 +453,7 @@ export default class CountlyAnalytics {
window.removeEventListener("scroll", this.onUserActivity); window.removeEventListener("scroll", this.onUserActivity);
} }
public reportFeedback(rating: 1 | 2 | 3 | 4 | 5, comment: string) { public reportFeedback(rating: Rating, comment: string) {
this.track<IStarRatingEvent>("[CLY]_star_rating", { rating, comment }, null, {}, true); this.track<IStarRatingEvent>("[CLY]_star_rating", { rating, comment }, null, {}, true);
} }
@ -536,7 +538,7 @@ export default class CountlyAnalytics {
// sanitize the error from identifiers // sanitize the error from identifiers
error = await strReplaceAsync(error, /([!@+#]).+?:[\w:.]+/g, async (substring: string, glyph: string) => { error = await strReplaceAsync(error, /([!@+#]).+?:[\w:.]+/g, async (substring: string, glyph: string) => {
return glyph + await hashHex(substring.substring(1)); return glyph + (await hashHex(substring.substring(1)));
}); });
const metrics = this.getMetrics(); const metrics = this.getMetrics();

View file

@ -35,6 +35,8 @@ import { isLoggedIn } from './components/structures/MatrixChat';
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { ActionPayload } from "./dispatcher/payloads"; import { ActionPayload } from "./dispatcher/payloads";
import { logger } from "matrix-js-sdk/src/logger";
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
export default class DeviceListener { export default class DeviceListener {
@ -100,7 +102,7 @@ export default class DeviceListener {
* @param {String[]} deviceIds List of device IDs to dismiss notifications for * @param {String[]} deviceIds List of device IDs to dismiss notifications for
*/ */
async dismissUnverifiedSessions(deviceIds: Iterable<string>) { async dismissUnverifiedSessions(deviceIds: Iterable<string>) {
console.log("Dismissing unverified sessions: " + Array.from(deviceIds).join(',')); logger.log("Dismissing unverified sessions: " + Array.from(deviceIds).join(','));
for (const d of deviceIds) { for (const d of deviceIds) {
this.dismissed.add(d); this.dismissed.add(d);
} }
@ -211,7 +213,7 @@ export default class DeviceListener {
private async recheck() { private async recheck() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) return; if (!(await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"))) return;
if (!cli.isCryptoEnabled()) return; if (!cli.isCryptoEnabled()) return;
// don't recheck until the initial sync is complete: lots of account data events will fire // don't recheck until the initial sync is complete: lots of account data events will fire
@ -286,8 +288,8 @@ export default class DeviceListener {
} }
} }
console.log("Old unverified sessions: " + Array.from(oldUnverifiedDeviceIds).join(',')); logger.log("Old unverified sessions: " + Array.from(oldUnverifiedDeviceIds).join(','));
console.log("New unverified sessions: " + Array.from(newUnverifiedDeviceIds).join(',')); logger.log("New unverified sessions: " + Array.from(newUnverifiedDeviceIds).join(','));
// Display or hide the batch toast for old unverified sessions // Display or hide the batch toast for old unverified sessions
if (oldUnverifiedDeviceIds.size > 0) { if (oldUnverifiedDeviceIds.size > 0) {

View file

@ -29,6 +29,8 @@ import {
} from './utils/IdentityServerUtils'; } from './utils/IdentityServerUtils';
import { abbreviateUrl } from './utils/UrlUtils'; import { abbreviateUrl } from './utils/UrlUtils';
import { logger } from "matrix-js-sdk/src/logger";
export class AbortedIdentityActionError extends Error {} export class AbortedIdentityActionError extends Error {}
export default class IdentityAuthClient { export default class IdentityAuthClient {
@ -127,7 +129,7 @@ export default class IdentityAuthClient {
await this._matrixClient.getIdentityAccount(token); await this._matrixClient.getIdentityAccount(token);
} catch (e) { } catch (e) {
if (e.errcode === "M_TERMS_NOT_SIGNED") { if (e.errcode === "M_TERMS_NOT_SIGNED") {
console.log("Identity server requires new terms to be agreed to"); logger.log("Identity server requires new terms to be agreed to");
await startTermsFlow([new Service( await startTermsFlow([new Service(
SERVICE_TYPES.IS, SERVICE_TYPES.IS,
identityServerUrl, identityServerUrl,
@ -141,7 +143,7 @@ export default class IdentityAuthClient {
if ( if (
!this.tempClient && !this.tempClient &&
!doesAccountDataHaveIdentityServer() && !doesAccountDataHaveIdentityServer() &&
!await doesIdentityServerHaveTerms(identityServerUrl) !(await doesIdentityServerHaveTerms(identityServerUrl))
) { ) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '', const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '',

View file

@ -58,6 +58,8 @@ import LazyLoadingDisabledDialog from "./components/views/dialogs/LazyLoadingDis
import SessionRestoreErrorDialog from "./components/views/dialogs/SessionRestoreErrorDialog"; import SessionRestoreErrorDialog from "./components/views/dialogs/SessionRestoreErrorDialog";
import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialog"; import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialog";
import { logger } from "matrix-js-sdk/src/logger";
const HOMESERVER_URL_KEY = "mx_hs_url"; const HOMESERVER_URL_KEY = "mx_hs_url";
const ID_SERVER_URL_KEY = "mx_is_url"; const ID_SERVER_URL_KEY = "mx_is_url";
@ -118,7 +120,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
fragmentQueryParams.guest_user_id && fragmentQueryParams.guest_user_id &&
fragmentQueryParams.guest_access_token fragmentQueryParams.guest_access_token
) { ) {
console.log("Using guest access credentials"); logger.log("Using guest access credentials");
return doSetLoggedIn({ return doSetLoggedIn({
userId: fragmentQueryParams.guest_user_id as string, userId: fragmentQueryParams.guest_user_id as string,
accessToken: fragmentQueryParams.guest_access_token as string, accessToken: fragmentQueryParams.guest_access_token as string,
@ -204,7 +206,7 @@ export function attemptTokenLogin(
initial_device_display_name: defaultDeviceDisplayName, initial_device_display_name: defaultDeviceDisplayName,
}, },
).then(function(creds) { ).then(function(creds) {
console.log("Logged in with token"); logger.log("Logged in with token");
return clearStorage().then(async () => { return clearStorage().then(async () => {
await persistCredentials(creds); await persistCredentials(creds);
// remember that we just logged in // remember that we just logged in
@ -273,7 +275,7 @@ function registerAsGuest(
isUrl: string, isUrl: string,
defaultDeviceDisplayName: string, defaultDeviceDisplayName: string,
): Promise<boolean> { ): Promise<boolean> {
console.log(`Doing guest login on ${hsUrl}`); logger.log(`Doing guest login on ${hsUrl}`);
// create a temporary MatrixClient to do the login // create a temporary MatrixClient to do the login
const client = createClient({ const client = createClient({
@ -285,7 +287,7 @@ function registerAsGuest(
initial_device_display_name: defaultDeviceDisplayName, initial_device_display_name: defaultDeviceDisplayName,
}, },
}).then((creds) => { }).then((creds) => {
console.log(`Registered as guest: ${creds.user_id}`); logger.log(`Registered as guest: ${creds.user_id}`);
return doSetLoggedIn({ return doSetLoggedIn({
userId: creds.user_id, userId: creds.user_id,
deviceId: creds.device_id, deviceId: creds.device_id,
@ -411,27 +413,27 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }):
if (accessToken && userId && hsUrl) { if (accessToken && userId && hsUrl) {
if (ignoreGuest && isGuest) { if (ignoreGuest && isGuest) {
console.log("Ignoring stored guest account: " + userId); logger.log("Ignoring stored guest account: " + userId);
return false; return false;
} }
let decryptedAccessToken = accessToken; let decryptedAccessToken = accessToken;
const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId); const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId);
if (pickleKey) { if (pickleKey) {
console.log("Got pickle key"); logger.log("Got pickle key");
if (typeof accessToken !== "string") { if (typeof accessToken !== "string") {
const encrKey = await pickleKeyToAesKey(pickleKey); const encrKey = await pickleKeyToAesKey(pickleKey);
decryptedAccessToken = await decryptAES(accessToken, encrKey, "access_token"); decryptedAccessToken = await decryptAES(accessToken, encrKey, "access_token");
encrKey.fill(0); encrKey.fill(0);
} }
} else { } else {
console.log("No pickle key available"); logger.log("No pickle key available");
} }
const freshLogin = sessionStorage.getItem("mx_fresh_login") === "true"; const freshLogin = sessionStorage.getItem("mx_fresh_login") === "true";
sessionStorage.removeItem("mx_fresh_login"); sessionStorage.removeItem("mx_fresh_login");
console.log(`Restoring session for ${userId}`); logger.log(`Restoring session for ${userId}`);
await doSetLoggedIn({ await doSetLoggedIn({
userId: userId, userId: userId,
deviceId: deviceId, deviceId: deviceId,
@ -444,7 +446,7 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }):
}, false); }, false);
return true; return true;
} else { } else {
console.log("No previous session found."); logger.log("No previous session found.");
return false; return false;
} }
} }
@ -488,9 +490,9 @@ export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<Matr
: null; : null;
if (pickleKey) { if (pickleKey) {
console.log("Created pickle key"); logger.log("Created pickle key");
} else { } else {
console.log("Pickle key not created"); logger.log("Pickle key not created");
} }
return doSetLoggedIn(Object.assign({}, credentials, { pickleKey }), true); return doSetLoggedIn(Object.assign({}, credentials, { pickleKey }), true);
@ -544,7 +546,7 @@ async function doSetLoggedIn(
const softLogout = isSoftLogout(); const softLogout = isSoftLogout();
console.log( logger.log(
"setLoggedIn: mxid: " + credentials.userId + "setLoggedIn: mxid: " + credentials.userId +
" deviceId: " + credentials.deviceId + " deviceId: " + credentials.deviceId +
" guest: " + credentials.guest + " guest: " + credentials.guest +
@ -689,7 +691,7 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise<void
SecurityCustomisations.persistCredentials?.(credentials); SecurityCustomisations.persistCredentials?.(credentials);
console.log(`Session persisted for ${credentials.userId}`); logger.log(`Session persisted for ${credentials.userId}`);
} }
let _isLoggingOut = false; let _isLoggingOut = false;
@ -726,7 +728,7 @@ export function logout(): void {
// token still valid, but we should fix this by having access // token still valid, but we should fix this by having access
// tokens expire (and if you really think you've been compromised, // tokens expire (and if you really think you've been compromised,
// change your password). // change your password).
console.log("Failed to call logout API: token will not be invalidated"); logger.log("Failed to call logout API: token will not be invalidated");
onLoggedOut(); onLoggedOut();
}, },
); );
@ -742,7 +744,7 @@ export function softLogout(): void {
// Dev note: please keep this log line around. It can be useful for track down // Dev note: please keep this log line around. It can be useful for track down
// random clients stopping in the middle of the logs. // random clients stopping in the middle of the logs.
console.log("Soft logout initiated"); logger.log("Soft logout initiated");
_isLoggingOut = true; // to avoid repeated flags _isLoggingOut = true; // to avoid repeated flags
// Ensure that we dispatch a view change **before** stopping the client so // Ensure that we dispatch a view change **before** stopping the client so
// so that React components unmount first. This avoids React soft crashes // so that React components unmount first. This avoids React soft crashes
@ -768,7 +770,7 @@ export function isLoggingOut(): boolean {
* syncing the client. * syncing the client.
*/ */
async function startMatrixClient(startSyncing = true): Promise<void> { async function startMatrixClient(startSyncing = true): Promise<void> {
console.log(`Lifecycle: Starting MatrixClient`); logger.log(`Lifecycle: Starting MatrixClient`);
// dispatch this before starting the matrix client: it's used // dispatch this before starting the matrix client: it's used
// to add listeners for the 'sync' event so otherwise we'd have // to add listeners for the 'sync' event so otherwise we'd have
@ -784,7 +786,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
UserActivity.sharedInstance().start(); UserActivity.sharedInstance().start();
DMRoomMap.makeShared().start(); DMRoomMap.makeShared().start();
IntegrationManagers.sharedInstance().startWatching(); IntegrationManagers.sharedInstance().startWatching();
ActiveWidgetStore.start(); ActiveWidgetStore.instance.start();
CallHandler.sharedInstance().start(); CallHandler.sharedInstance().start();
// Start Mjolnir even though we haven't checked the feature flag yet. Starting // Start Mjolnir even though we haven't checked the feature flag yet. Starting
@ -890,7 +892,7 @@ export function stopMatrixClient(unsetClient = true): void {
UserActivity.sharedInstance().stop(); UserActivity.sharedInstance().stop();
TypingStore.sharedInstance().reset(); TypingStore.sharedInstance().reset();
Presence.stop(); Presence.stop();
ActiveWidgetStore.stop(); ActiveWidgetStore.instance.stop();
IntegrationManagers.sharedInstance().stopWatching(); IntegrationManagers.sharedInstance().stopWatching();
Mjolnir.sharedInstance().stop(); Mjolnir.sharedInstance().stop();
DeviceListener.sharedInstance().stop(); DeviceListener.sharedInstance().stop();

View file

@ -21,6 +21,8 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
import { IMatrixClientCreds } from "./MatrixClientPeg"; import { IMatrixClientCreds } from "./MatrixClientPeg";
import SecurityCustomisations from "./customisations/Security"; import SecurityCustomisations from "./customisations/Security";
import { logger } from "matrix-js-sdk/src/logger";
interface ILoginOptions { interface ILoginOptions {
defaultDeviceDisplayName?: string; defaultDeviceDisplayName?: string;
} }
@ -166,7 +168,7 @@ export default class Login {
return sendLoginRequest( return sendLoginRequest(
this.fallbackHsUrl, this.isUrl, 'm.login.password', loginParams, this.fallbackHsUrl, this.isUrl, 'm.login.password', loginParams,
).catch((fallbackError) => { ).catch((fallbackError) => {
console.log("fallback HS login failed", fallbackError); logger.log("fallback HS login failed", fallbackError);
// throw the original error // throw the original error
throw originalError; throw originalError;
}); });
@ -184,7 +186,7 @@ export default class Login {
} }
throw originalLoginError; throw originalLoginError;
}).catch((error) => { }).catch((error) => {
console.log("Login failed", error); logger.log("Login failed", error);
throw error; throw error;
}); });
} }
@ -218,12 +220,12 @@ export async function sendLoginRequest(
if (wellknown) { if (wellknown) {
if (wellknown["m.homeserver"] && wellknown["m.homeserver"]["base_url"]) { if (wellknown["m.homeserver"] && wellknown["m.homeserver"]["base_url"]) {
hsUrl = wellknown["m.homeserver"]["base_url"]; hsUrl = wellknown["m.homeserver"]["base_url"];
console.log(`Overrode homeserver setting with ${hsUrl} from login response`); logger.log(`Overrode homeserver setting with ${hsUrl} from login response`);
} }
if (wellknown["m.identity_server"] && wellknown["m.identity_server"]["base_url"]) { if (wellknown["m.identity_server"] && wellknown["m.identity_server"]["base_url"]) {
// TODO: should we prompt here? // TODO: should we prompt here?
isUrl = wellknown["m.identity_server"]["base_url"]; isUrl = wellknown["m.identity_server"]["base_url"];
console.log(`Overrode IS setting with ${isUrl} from login response`); logger.log(`Overrode IS setting with ${isUrl} from login response`);
} }
} }

View file

@ -36,6 +36,8 @@ import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } fro
import { SHOW_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode"; import { SHOW_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode";
import SecurityCustomisations from "./customisations/Security"; import SecurityCustomisations from "./customisations/Security";
import { logger } from "matrix-js-sdk/src/logger";
export interface IMatrixClientCreds { export interface IMatrixClientCreds {
homeserverUrl: string; homeserverUrl: string;
identityServerUrl: string; identityServerUrl: string;
@ -166,7 +168,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
for (const dbType of ['indexeddb', 'memory']) { for (const dbType of ['indexeddb', 'memory']) {
try { try {
const promise = this.matrixClient.store.startup(); const promise = this.matrixClient.store.startup();
console.log("MatrixClientPeg: waiting for MatrixClient store to initialise"); logger.log("MatrixClientPeg: waiting for MatrixClient store to initialise");
await promise; await promise;
break; break;
} catch (err) { } catch (err) {
@ -225,9 +227,9 @@ class MatrixClientPegClass implements IMatrixClientPeg {
public async start(): Promise<any> { public async start(): Promise<any> {
const opts = await this.assign(); const opts = await this.assign();
console.log(`MatrixClientPeg: really starting MatrixClient`); logger.log(`MatrixClientPeg: really starting MatrixClient`);
await this.get().startClient(opts); await this.get().startClient(opts);
console.log(`MatrixClientPeg: MatrixClient started`); logger.log(`MatrixClientPeg: MatrixClient started`);
} }
public getCredentials(): IMatrixClientCreds { public getCredentials(): IMatrixClientCreds {

View file

@ -38,6 +38,8 @@ import UserActivity from "./UserActivity";
import { mediaFromMxc } from "./customisations/Media"; import { mediaFromMxc } from "./customisations/Media";
import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import { logger } from "matrix-js-sdk/src/logger";
/* /*
* Dispatches: * Dispatches:
* { * {
@ -160,7 +162,7 @@ export const Notifier = {
_playAudioNotification: async function(ev: MatrixEvent, room: Room) { _playAudioNotification: async function(ev: MatrixEvent, room: Room) {
const sound = this.getSoundForRoom(room.roomId); const sound = this.getSoundForRoom(room.roomId);
console.log(`Got sound ${sound && sound.name || "default"} for ${room.roomId}`); logger.log(`Got sound ${sound && sound.name || "default"} for ${room.roomId}`);
try { try {
const selector = const selector =

View file

@ -21,6 +21,8 @@ import SettingsStore from './settings/SettingsStore';
import { MatrixClientPeg } from "./MatrixClientPeg"; import { MatrixClientPeg } from "./MatrixClientPeg";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger";
/* Posthog analytics tracking. /* Posthog analytics tracking.
* *
* Anonymity behaviour is as follows: * Anonymity behaviour is as follows:
@ -175,7 +177,7 @@ export class PosthogAnalytics {
// $redacted_current_url is injected by this class earlier in capture(), as its generation // $redacted_current_url is injected by this class earlier in capture(), as its generation
// is async and can't be done in this non-async callback. // is async and can't be done in this non-async callback.
if (!properties['$redacted_current_url']) { if (!properties['$redacted_current_url']) {
console.log("$redacted_current_url not set in sanitizeProperties, will drop $current_url entirely"); logger.log("$redacted_current_url not set in sanitizeProperties, will drop $current_url entirely");
} }
properties['$current_url'] = properties['$redacted_current_url']; properties['$current_url'] = properties['$redacted_current_url'];
delete properties['$redacted_current_url']; delete properties['$redacted_current_url'];
@ -291,7 +293,7 @@ export class PosthogAnalytics {
} catch (e) { } catch (e) {
// The above could fail due to network requests, but not essential to starting the application, // The above could fail due to network requests, but not essential to starting the application,
// so swallow it. // so swallow it.
console.log("Unable to identify user for tracking" + e.toString()); logger.log("Unable to identify user for tracking" + e.toString());
} }
} }
} }

View file

@ -20,6 +20,8 @@ import { Room } from 'matrix-js-sdk/src/models/room';
import { MatrixClientPeg } from './MatrixClientPeg'; import { MatrixClientPeg } from './MatrixClientPeg';
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
import { logger } from "matrix-js-sdk/src/logger";
export default class Resend { export default class Resend {
static resendUnsentEvents(room: Room): Promise<void[]> { static resendUnsentEvents(room: Room): Promise<void[]> {
return Promise.all(room.getPendingEvents().filter(function(ev: MatrixEvent) { return Promise.all(room.getPendingEvents().filter(function(ev: MatrixEvent) {
@ -47,7 +49,7 @@ export default class Resend {
}, function(err: Error) { }, function(err: Error) {
// 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 + ')'); logger.log('Resend got send failure: ' + err.name + '(' + err + ')');
}); });
} }

View file

@ -25,6 +25,8 @@ import { WidgetType } from "./widgets/WidgetType";
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types"; import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
// The version of the integration manager API we're intending to work with // The version of the integration manager API we're intending to work with
const imApiVersion = "1.1"; const imApiVersion = "1.1";
@ -136,7 +138,7 @@ export default class ScalarAuthClient {
return token; return token;
}).catch((e) => { }).catch((e) => {
if (e instanceof TermsNotSignedError) { if (e instanceof TermsNotSignedError) {
console.log("Integration manager requires new terms to be agreed to"); logger.log("Integration manager requires new terms to be agreed to");
// The terms endpoints are new and so live on standard _matrix prefixes, // The terms endpoints are new and so live on standard _matrix prefixes,
// but IM rest urls are currently configured with paths, so remove the // but IM rest urls are currently configured with paths, so remove the
// path from the base URL before passing it to the js-sdk // path from the base URL before passing it to the js-sdk

View file

@ -245,6 +245,8 @@ import { IntegrationManagers } from "./integrations/IntegrationManagers";
import { WidgetType } from "./widgets/WidgetType"; import { WidgetType } from "./widgets/WidgetType";
import { objectClone } from "./utils/objects"; import { objectClone } from "./utils/objects";
import { logger } from "matrix-js-sdk/src/logger";
function sendResponse(event, res) { function sendResponse(event, res) {
const data = objectClone(event.data); const data = objectClone(event.data);
data.response = res; data.response = res;
@ -266,7 +268,7 @@ function sendError(event, msg, nestedError) {
} }
function inviteUser(event, roomId, userId) { function inviteUser(event, roomId, userId) {
console.log(`Received request to invite ${userId} into room ${roomId}`); logger.log(`Received request to invite ${userId} into room ${roomId}`);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, _t('You need to be logged in.')); sendError(event, _t('You need to be logged in.'));
@ -400,7 +402,7 @@ function setPlumbingState(event, roomId, status) {
if (typeof status !== 'string') { if (typeof status !== 'string') {
throw new Error('Plumbing state status should be a string'); throw new Error('Plumbing state status should be a string');
} }
console.log(`Received request to set plumbing state to status "${status}" in room ${roomId}`); logger.log(`Received request to set plumbing state to status "${status}" in room ${roomId}`);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, _t('You need to be logged in.')); sendError(event, _t('You need to be logged in.'));
@ -416,7 +418,7 @@ function setPlumbingState(event, roomId, status) {
} }
function setBotOptions(event, roomId, userId) { function setBotOptions(event, roomId, userId) {
console.log(`Received request to set options for bot ${userId} in room ${roomId}`); logger.log(`Received request to set options for bot ${userId} in room ${roomId}`);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, _t('You need to be logged in.')); sendError(event, _t('You need to be logged in.'));
@ -437,7 +439,7 @@ function setBotPower(event, roomId, userId, level) {
return; return;
} }
console.log(`Received request to set power level to ${level} for bot ${userId} in room ${roomId}.`); logger.log(`Received request to set power level to ${level} for bot ${userId} in room ${roomId}.`);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!client) { if (!client) {
sendError(event, _t('You need to be logged in.')); sendError(event, _t('You need to be logged in.'));
@ -463,17 +465,17 @@ function setBotPower(event, roomId, userId, level) {
} }
function getMembershipState(event, roomId, userId) { function getMembershipState(event, roomId, userId) {
console.log(`membership_state of ${userId} in room ${roomId} requested.`); logger.log(`membership_state of ${userId} in room ${roomId} requested.`);
returnStateEvent(event, roomId, "m.room.member", userId); returnStateEvent(event, roomId, "m.room.member", userId);
} }
function getJoinRules(event, roomId) { function getJoinRules(event, roomId) {
console.log(`join_rules of ${roomId} requested.`); logger.log(`join_rules of ${roomId} requested.`);
returnStateEvent(event, roomId, "m.room.join_rules", ""); returnStateEvent(event, roomId, "m.room.join_rules", "");
} }
function botOptions(event, roomId, userId) { function botOptions(event, roomId, userId) {
console.log(`bot_options of ${userId} in room ${roomId} requested.`); logger.log(`bot_options of ${userId} in room ${roomId} requested.`);
returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId); returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
} }

View file

@ -31,6 +31,8 @@ import SettingsStore from "./settings/SettingsStore";
import SecurityCustomisations from "./customisations/Security"; import SecurityCustomisations from "./customisations/Security";
import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning'; import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning';
import { logger } from "matrix-js-sdk/src/logger";
// This stores the secret storage private keys in memory for the JS SDK. This is // This stores the secret storage private keys in memory for the JS SDK. This is
// only meant to act as a cache to avoid prompting the user multiple times // only meant to act as a cache to avoid prompting the user multiple times
// during the same single operation. Use `accessSecretStorage` below to scope a // during the same single operation. Use `accessSecretStorage` below to scope a
@ -136,7 +138,7 @@ async function getSecretStorageKey(
const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.(); const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
if (keyFromCustomisations) { if (keyFromCustomisations) {
console.log("Using key from security customisations (secret storage)"); logger.log("Using key from security customisations (secret storage)");
cacheSecretStorageKey(keyId, keyInfo, keyFromCustomisations); cacheSecretStorageKey(keyId, keyInfo, keyFromCustomisations);
return [keyId, keyFromCustomisations]; return [keyId, keyFromCustomisations];
} }
@ -186,7 +188,7 @@ export async function getDehydrationKey(
): Promise<Uint8Array> { ): Promise<Uint8Array> {
const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.(); const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
if (keyFromCustomisations) { if (keyFromCustomisations) {
console.log("Using key from security customisations (dehydration)"); logger.log("Using key from security customisations (dehydration)");
return keyFromCustomisations; return keyFromCustomisations;
} }
@ -248,13 +250,13 @@ async function onSecretRequested(
name: string, name: string,
deviceTrust: DeviceTrustLevel, deviceTrust: DeviceTrustLevel,
): Promise<string> { ): Promise<string> {
console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust); logger.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (userId !== client.getUserId()) { if (userId !== client.getUserId()) {
return; return;
} }
if (!deviceTrust || !deviceTrust.isVerified()) { if (!deviceTrust || !deviceTrust.isVerified()) {
console.log(`Ignoring secret request from untrusted device ${deviceId}`); logger.log(`Ignoring secret request from untrusted device ${deviceId}`);
return; return;
} }
if ( if (
@ -267,7 +269,7 @@ async function onSecretRequested(
const keyId = name.replace("m.cross_signing.", ""); const keyId = name.replace("m.cross_signing.", "");
const key = await callbacks.getCrossSigningKeyCache(keyId); const key = await callbacks.getCrossSigningKeyCache(keyId);
if (!key) { if (!key) {
console.log( logger.log(
`${keyId} requested by ${deviceId}, but not found in cache`, `${keyId} requested by ${deviceId}, but not found in cache`,
); );
} }
@ -275,7 +277,7 @@ async function onSecretRequested(
} else if (name === "m.megolm_backup.v1") { } else if (name === "m.megolm_backup.v1") {
const key = await client.crypto.getSessionBackupPrivateKey(); const key = await client.crypto.getSessionBackupPrivateKey();
if (!key) { if (!key) {
console.log( logger.log(
`session backup key requested by ${deviceId}, but not found in cache`, `session backup key requested by ${deviceId}, but not found in cache`,
); );
} }
@ -329,7 +331,7 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
secretStorageBeingAccessed = true; secretStorageBeingAccessed = true;
try { try {
if (!await cli.hasSecretStorageKey() || forceReset) { if (!(await cli.hasSecretStorageKey()) || forceReset) {
// This dialog calls bootstrap itself after guiding the user through // This dialog calls bootstrap itself after guiding the user through
// passphrase creation. // passphrase creation.
const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '', const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '',
@ -383,12 +385,12 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
if (secretStorageKeyInfo[keyId] && secretStorageKeyInfo[keyId].passphrase) { if (secretStorageKeyInfo[keyId] && secretStorageKeyInfo[keyId].passphrase) {
dehydrationKeyInfo = { passphrase: secretStorageKeyInfo[keyId].passphrase }; dehydrationKeyInfo = { passphrase: secretStorageKeyInfo[keyId].passphrase };
} }
console.log("Setting dehydration key"); logger.log("Setting dehydration key");
await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo, "Backup device"); await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo, "Backup device");
} else if (!keyId) { } else if (!keyId) {
console.warn("Not setting dehydration key: no SSSS key found"); console.warn("Not setting dehydration key: no SSSS key found");
} else { } else {
console.log("Not setting dehydration key: feature disabled"); logger.log("Not setting dehydration key: feature disabled");
} }
} }
@ -416,8 +418,8 @@ export async function tryToUnlockSecretStorageWithDehydrationKey(
): Promise<void> { ): Promise<void> {
const key = dehydrationCache.key; const key = dehydrationCache.key;
let restoringBackup = false; let restoringBackup = false;
if (key && await client.isSecretStorageReady()) { if (key && (await client.isSecretStorageReady())) {
console.log("Trying to set up cross-signing using dehydration key"); logger.log("Trying to set up cross-signing using dehydration key");
secretStorageBeingAccessed = true; secretStorageBeingAccessed = true;
nonInteractive = true; nonInteractive = true;
try { try {

View file

@ -55,6 +55,8 @@ import RoomUpgradeWarningDialog from "./components/views/dialogs/RoomUpgradeWarn
import InfoDialog from "./components/views/dialogs/InfoDialog"; import InfoDialog from "./components/views/dialogs/InfoDialog";
import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog"; import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog";
import { logger } from "matrix-js-sdk/src/logger";
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
interface HTMLInputEvent extends Event { interface HTMLInputEvent extends Event {
target: HTMLInputElement & EventTarget; target: HTMLInputElement & EventTarget;
@ -291,7 +293,7 @@ export const Commands = [
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const ev = cli.getRoom(roomId).currentState.getStateEvents('m.room.member', cli.getUserId()); const ev = cli.getRoom(roomId).currentState.getStateEvents('m.room.member', cli.getUserId());
const content = { const content = {
...ev ? ev.getContent() : { membership: 'join' }, ...(ev ? ev.getContent() : { membership: 'join' }),
displayname: args, displayname: args,
}; };
return success(cli.sendStateEvent(roomId, 'm.room.member', content, cli.getUserId())); return success(cli.sendStateEvent(roomId, 'm.room.member', content, cli.getUserId()));
@ -335,7 +337,7 @@ export const Commands = [
if (!url) return; if (!url) return;
const ev = room.currentState.getStateEvents('m.room.member', userId); const ev = room.currentState.getStateEvents('m.room.member', userId);
const content = { const content = {
...ev ? ev.getContent() : { membership: 'join' }, ...(ev ? ev.getContent() : { membership: 'join' }),
avatar_url: url, avatar_url: url,
}; };
return cli.sendStateEvent(roomId, 'm.room.member', content, userId); return cli.sendStateEvent(roomId, 'm.room.member', content, userId);
@ -801,7 +803,7 @@ export const Commands = [
const iframe = embed.childNodes[0] as ChildElement; const iframe = embed.childNodes[0] as ChildElement;
if (iframe.tagName.toLowerCase() === 'iframe' && iframe.attrs) { if (iframe.tagName.toLowerCase() === 'iframe' && iframe.attrs) {
const srcAttr = iframe.attrs.find(a => a.name === 'src'); const srcAttr = iframe.attrs.find(a => a.name === 'src');
console.log("Pulling URL out of iframe (embed code)"); logger.log("Pulling URL out of iframe (embed code)");
widgetUrl = srcAttr.value; widgetUrl = srcAttr.value;
} }
} }
@ -821,7 +823,7 @@ export const Commands = [
// Make the widget a Jitsi widget if it looks like a Jitsi widget // Make the widget a Jitsi widget if it looks like a Jitsi widget
const jitsiData = Jitsi.getInstance().parsePreferredConferenceUrl(widgetUrl); const jitsiData = Jitsi.getInstance().parsePreferredConferenceUrl(widgetUrl);
if (jitsiData) { if (jitsiData) {
console.log("Making /addwidget widget a Jitsi conference"); logger.log("Making /addwidget widget a Jitsi conference");
type = WidgetType.JITSI; type = WidgetType.JITSI;
name = "Jitsi Conference"; name = "Jitsi Conference";
data = jitsiData; data = jitsiData;

View file

@ -21,6 +21,8 @@ import { MatrixClientPeg } from './MatrixClientPeg';
import * as sdk from '.'; import * as sdk from '.';
import Modal from './Modal'; import Modal from './Modal';
import { logger } from "matrix-js-sdk/src/logger";
export class TermsNotSignedError extends Error {} export class TermsNotSignedError extends Error {}
/** /**
@ -140,11 +142,11 @@ export async function startTermsFlow(
const numAcceptedBeforeAgreement = agreedUrlSet.size; const numAcceptedBeforeAgreement = agreedUrlSet.size;
if (unagreedPoliciesAndServicePairs.length > 0) { if (unagreedPoliciesAndServicePairs.length > 0) {
const newlyAgreedUrls = await interactionCallback(unagreedPoliciesAndServicePairs, [...agreedUrlSet]); const newlyAgreedUrls = await interactionCallback(unagreedPoliciesAndServicePairs, [...agreedUrlSet]);
console.log("User has agreed to URLs", newlyAgreedUrls); logger.log("User has agreed to URLs", newlyAgreedUrls);
// Merge with previously agreed URLs // Merge with previously agreed URLs
newlyAgreedUrls.forEach(url => agreedUrlSet.add(url)); newlyAgreedUrls.forEach(url => agreedUrlSet.add(url));
} else { } else {
console.log("User has already agreed to all required policies"); logger.log("User has already agreed to all required policies");
} }
// We only ever add to the set of URLs, so if anything has changed then we'd see a different length // We only ever add to the set of URLs, so if anything has changed then we'd see a different length
@ -188,7 +190,7 @@ export function dialogTermsInteractionCallback(
extraClassNames?: string, extraClassNames?: string,
): Promise<string[]> { ): Promise<string[]> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
console.log("Terms that need agreement", policiesAndServicePairs); logger.log("Terms that need agreement", policiesAndServicePairs);
// FIXME: Using an import will result in test failures // FIXME: Using an import will result in test failures
const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog"); const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog");

View file

@ -20,6 +20,8 @@ import DMRoomMap from "./utils/DMRoomMap";
import CallHandler, { VIRTUAL_ROOM_EVENT_TYPE } from './CallHandler'; import CallHandler, { VIRTUAL_ROOM_EVENT_TYPE } from './CallHandler';
import { Room } from 'matrix-js-sdk/src/models/room'; import { Room } from 'matrix-js-sdk/src/models/room';
import { logger } from "matrix-js-sdk/src/logger";
// Functions for mapping virtual users & rooms. Currently the only lookup // Functions for mapping virtual users & rooms. Currently the only lookup
// is sip virtual: there could be others in the future. // is sip virtual: there could be others in the future.
@ -59,7 +61,7 @@ export default class VoipUserMapper {
public nativeRoomForVirtualRoom(roomId: string): string { public nativeRoomForVirtualRoom(roomId: string): string {
const cachedNativeRoomId = this.virtualToNativeRoomIdCache.get(roomId); const cachedNativeRoomId = this.virtualToNativeRoomIdCache.get(roomId);
if (cachedNativeRoomId) { if (cachedNativeRoomId) {
console.log( logger.log(
"Returning native room ID " + cachedNativeRoomId + " for virtual room ID " + roomId + " from cache", "Returning native room ID " + cachedNativeRoomId + " for virtual room ID " + roomId + " from cache",
); );
return cachedNativeRoomId; return cachedNativeRoomId;
@ -98,7 +100,7 @@ export default class VoipUserMapper {
if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return; if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return;
const inviterId = invitedRoom.getDMInviter(); const inviterId = invitedRoom.getDMInviter();
console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`); logger.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId); const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId);
if (result.length === 0) { if (result.length === 0) {
return; return;

View file

@ -26,10 +26,9 @@ import { SettingLevel } from "../../../../settings/SettingLevel";
import Field from '../../../../components/views/elements/Field'; import Field from '../../../../components/views/elements/Field';
import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import DialogButtons from "../../../../components/views/elements/DialogButtons"; import DialogButtons from "../../../../components/views/elements/DialogButtons";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
interface IProps { interface IProps extends IDialogProps {}
onFinished: (confirmed: boolean) => void;
}
interface IState { interface IState {
eventIndexSize: number; eventIndexSize: number;

View file

@ -34,6 +34,8 @@ import RestoreKeyBackupDialog from "../../../../components/views/dialogs/securit
import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils'; import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils';
import SecurityCustomisations from "../../../../customisations/Security"; import SecurityCustomisations from "../../../../customisations/Security";
import { logger } from "matrix-js-sdk/src/logger";
const PHASE_LOADING = 0; const PHASE_LOADING = 0;
const PHASE_LOADERROR = 1; const PHASE_LOADERROR = 1;
const PHASE_CHOOSE_KEY_PASSPHRASE = 2; const PHASE_CHOOSE_KEY_PASSPHRASE = 2;
@ -122,7 +124,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_getInitialPhase() { _getInitialPhase() {
const keyFromCustomisations = SecurityCustomisations.createSecretStorageKey?.(); const keyFromCustomisations = SecurityCustomisations.createSecretStorageKey?.();
if (keyFromCustomisations) { if (keyFromCustomisations) {
console.log("Created key via customisations, jumping to bootstrap step"); logger.log("Created key via customisations, jumping to bootstrap step");
this._recoveryKey = { this._recoveryKey = {
privateKey: keyFromCustomisations, privateKey: keyFromCustomisations,
}; };
@ -138,7 +140,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
const backupSigStatus = ( const backupSigStatus = (
// we may not have started crypto yet, in which case we definitely don't trust the backup // we may not have started crypto yet, in which case we definitely don't trust the backup
MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo) MatrixClientPeg.get().isCryptoEnabled() && (await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo))
); );
const { forceReset } = this.props; const { forceReset } = this.props;
@ -165,10 +167,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
// We should never get here: the server should always require // We should never get here: the server should always require
// UI auth to upload device signing keys. If we do, we upload // UI auth to upload device signing keys. If we do, we upload
// no keys which would be a no-op. // no keys which would be a no-op.
console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!"); logger.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
} catch (error) { } catch (error) {
if (!error.data || !error.data.flows) { if (!error.data || !error.data.flows) {
console.log("uploadDeviceSigningKeys advertised no flows!"); logger.log("uploadDeviceSigningKeys advertised no flows!");
return; return;
} }
const canUploadKeysWithPasswordOnly = error.data.flows.some(f => { const canUploadKeysWithPasswordOnly = error.data.flows.some(f => {
@ -304,7 +306,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
try { try {
if (forceReset) { if (forceReset) {
console.log("Forcing secret storage reset"); logger.log("Forcing secret storage reset");
await cli.bootstrapSecretStorage({ await cli.bootstrapSecretStorage({
createSecretStorageKey: async () => this._recoveryKey, createSecretStorageKey: async () => this._recoveryKey,
setupNewKeyBackup: true, setupNewKeyBackup: true,

View file

@ -23,6 +23,8 @@ import { PlaybackClock } from "./PlaybackClock";
import { createAudioContext, decodeOgg } from "./compat"; import { createAudioContext, decodeOgg } from "./compat";
import { clamp } from "../utils/numbers"; import { clamp } from "../utils/numbers";
import { logger } from "matrix-js-sdk/src/logger";
export enum PlaybackState { export enum PlaybackState {
Decoding = "decoding", Decoding = "decoding",
Stopped = "stopped", // no progress on timeline Stopped = "stopped", // no progress on timeline
@ -139,7 +141,7 @@ export class Playback extends EventEmitter implements IDestroyable {
// audio buffer in memory, as that can balloon to far greater than the input buffer's // audio buffer in memory, as that can balloon to far greater than the input buffer's
// byte length. // byte length.
if (this.buf.byteLength > 5 * 1024 * 1024) { // 5mb if (this.buf.byteLength > 5 * 1024 * 1024) { // 5mb
console.log("Audio file too large: processing through <audio /> element"); logger.log("Audio file too large: processing through <audio /> element");
this.element = document.createElement("AUDIO") as HTMLAudioElement; this.element = document.createElement("AUDIO") as HTMLAudioElement;
const prom = new Promise((resolve, reject) => { const prom = new Promise((resolve, reject) => {
this.element.onloadeddata = () => resolve(null); this.element.onloadeddata = () => resolve(null);

View file

@ -21,6 +21,8 @@ import decoderWasmPath from 'opus-recorder/dist/decoderWorker.min.wasm';
import wavEncoderPath from 'opus-recorder/dist/waveWorker.min.js'; import wavEncoderPath from 'opus-recorder/dist/waveWorker.min.js';
import decoderPath from 'opus-recorder/dist/decoderWorker.min.js'; import decoderPath from 'opus-recorder/dist/decoderWorker.min.js';
import { logger } from "matrix-js-sdk/src/logger";
export function createAudioContext(opts?: AudioContextOptions): AudioContext { export function createAudioContext(opts?: AudioContextOptions): AudioContext {
if (window.AudioContext) { if (window.AudioContext) {
return new AudioContext(opts); return new AudioContext(opts);
@ -38,7 +40,7 @@ export function decodeOgg(audioBuffer: ArrayBuffer): Promise<ArrayBuffer> {
// Condensed version of decoder example, using a promise: // Condensed version of decoder example, using a promise:
// https://github.com/chris-rudmin/opus-recorder/blob/master/example/decoder.html // https://github.com/chris-rudmin/opus-recorder/blob/master/example/decoder.html
return new Promise((resolve) => { // no reject because the workers don't seem to have a fail path return new Promise((resolve) => { // no reject because the workers don't seem to have a fail path
console.log("Decoder WASM path: " + decoderWasmPath); // so we use the variable (avoid tree shake) logger.log("Decoder WASM path: " + decoderWasmPath); // so we use the variable (avoid tree shake)
const typedArray = new Uint8Array(audioBuffer); const typedArray = new Uint8Array(audioBuffer);
const decoderWorker = new Worker(decoderPath); const decoderWorker = new Worker(decoderPath);
const wavWorker = new Worker(wavEncoderPath); const wavWorker = new Worker(wavEncoderPath);

View file

@ -76,7 +76,6 @@ const LeftPanelWidget: React.FC = () => {
<AppTile <AppTile
app={app} app={app}
fullWidth fullWidth
show
showMenubar={false} showMenubar={false}
userWidget userWidget
userId={cli.getUserId()} userId={cli.getUserId()}

View file

@ -110,6 +110,8 @@ import { copyPlaintext } from "../../utils/strings";
import { PosthogAnalytics } from '../../PosthogAnalytics'; import { PosthogAnalytics } from '../../PosthogAnalytics';
import { initSentry } from "../../sentry"; import { initSentry } from "../../sentry";
import { logger } from "matrix-js-sdk/src/logger";
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
// a special initial state which is only used at startup, while we are // a special initial state which is only used at startup, while we are
@ -893,12 +895,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.focusComposer = true; this.focusComposer = true;
if (roomInfo.room_alias) { if (roomInfo.room_alias) {
console.log( logger.log(
`Switching to room alias ${roomInfo.room_alias} at event ` + `Switching to room alias ${roomInfo.room_alias} at event ` +
roomInfo.event_id, roomInfo.event_id,
); );
} else { } else {
console.log(`Switching to room id ${roomInfo.room_id} at event ` + logger.log(`Switching to room id ${roomInfo.room_id} at event ` +
roomInfo.event_id, roomInfo.event_id,
); );
} }
@ -1407,7 +1409,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// such as when laptops unsleep. // such as when laptops unsleep.
// https://github.com/vector-im/element-web/issues/3307#issuecomment-282895568 // https://github.com/vector-im/element-web/issues/3307#issuecomment-282895568
cli.setCanResetTimelineCallback((roomId) => { cli.setCanResetTimelineCallback((roomId) => {
console.log("Request to reset timeline in room ", roomId, " viewing:", this.state.currentRoomId); logger.log("Request to reset timeline in room ", roomId, " viewing:", this.state.currentRoomId);
if (roomId !== this.state.currentRoomId) { if (roomId !== this.state.currentRoomId) {
// It is safe to remove events from rooms we are not viewing. // It is safe to remove events from rooms we are not viewing.
return true; return true;

View file

@ -448,7 +448,9 @@ export default class MessagePanel extends React.Component<IProps, IState> {
// Always show highlighted event // Always show highlighted event
if (this.props.highlightedEventId === mxEv.getId()) return true; if (this.props.highlightedEventId === mxEv.getId()) return true;
if (mxEv.replyInThread // Checking if the message has a "parentEventId" as we do not
// want to hide the root event of the thread
if (mxEv.replyInThread && mxEv.parentEventId
&& this.props.hideThreadedMessages && this.props.hideThreadedMessages
&& SettingsStore.getValue("feature_thread")) { && SettingsStore.getValue("feature_thread")) {
return false; return false;

View file

@ -18,7 +18,6 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { RoomState } from "matrix-js-sdk/src/models/room-state"; import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { User } from "matrix-js-sdk/src/models/user";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
@ -54,11 +53,12 @@ import { throttle } from 'lodash';
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/SpaceStore";
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
import { E2EStatus } from '../../utils/ShieldUtils'; import { E2EStatus } from '../../utils/ShieldUtils';
import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload';
interface IProps { interface IProps {
room?: Room; // if showing panels for a given room, this is set room?: Room; // if showing panels for a given room, this is set
groupId?: string; // if showing panels for a given group, this is set groupId?: string; // if showing panels for a given group, this is set
user?: User; // used if we know the user ahead of opening the panel member?: RoomMember; // used if we know the room member ahead of opening the panel
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
permalinkCreator?: RoomPermalinkCreator; permalinkCreator?: RoomPermalinkCreator;
e2eStatus?: E2EStatus; e2eStatus?: E2EStatus;
@ -99,10 +99,10 @@ export default class RightPanel extends React.Component<IProps, IState> {
// Helper function to split out the logic for getPhaseFromProps() and the constructor // Helper function to split out the logic for getPhaseFromProps() and the constructor
// as both are called at the same time in the constructor. // as both are called at the same time in the constructor.
private getUserForPanel() { private getUserForPanel(): RoomMember {
if (this.state && this.state.member) return this.state.member; if (this.state && this.state.member) return this.state.member;
const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams; const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams;
return this.props.user || lastParams['member']; return this.props.member || lastParams['member'];
} }
// gets the current phase from the props and also maybe the store // gets the current phase from the props and also maybe the store
@ -143,14 +143,14 @@ export default class RightPanel extends React.Component<IProps, IState> {
return rps.roomPanelPhase; return rps.roomPanelPhase;
} }
componentDidMount() { public componentDidMount(): void {
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
const cli = this.context; const cli = this.context;
cli.on("RoomState.members", this.onRoomStateMember); cli.on("RoomState.members", this.onRoomStateMember);
this.initGroupStore(this.props.groupId); this.initGroupStore(this.props.groupId);
} }
componentWillUnmount() { public componentWillUnmount(): void {
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
if (this.context) { if (this.context) {
this.context.removeListener("RoomState.members", this.onRoomStateMember); this.context.removeListener("RoomState.members", this.onRoomStateMember);
@ -159,7 +159,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line public UNSAFE_componentWillReceiveProps(newProps: IProps): void { // eslint-disable-line
if (newProps.groupId !== this.props.groupId) { if (newProps.groupId !== this.props.groupId) {
this.unregisterGroupStore(); this.unregisterGroupStore();
this.initGroupStore(newProps.groupId); this.initGroupStore(newProps.groupId);
@ -196,6 +196,15 @@ export default class RightPanel extends React.Component<IProps, IState> {
}; };
private onAction = (payload: ActionPayload) => { private onAction = (payload: ActionPayload) => {
const isChangingRoom = payload.action === 'view_room' && payload.room_id !== this.props.room.roomId;
const isViewingThread = this.state.phase === RightPanelPhases.ThreadView;
if (isChangingRoom && isViewingThread) {
dis.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.ThreadPanel,
});
}
if (payload.action === Action.AfterRightPanelPhaseChange) { if (payload.action === Action.AfterRightPanelPhaseChange) {
this.setState({ this.setState({
phase: payload.phase, phase: payload.phase,
@ -215,7 +224,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
// XXX: There are three different ways of 'closing' this panel depending on what state // XXX: There are three different ways of 'closing' this panel depending on what state
// things are in... this knows far more than it should do about the state of the rest // things are in... this knows far more than it should do about the state of the rest
// of the app and is generally a bit silly. // of the app and is generally a bit silly.
if (this.props.user) { if (this.props.member) {
// If we have a user prop then we're displaying a user from the 'user' page type // If we have a user prop then we're displaying a user from the 'user' page type
// in LoggedInView, so need to change the page type to close the panel (we switch // in LoggedInView, so need to change the page type to close the panel (we switch
// to the home page which is not obviously the correct thing to do, but I'm not sure // to the home page which is not obviously the correct thing to do, but I'm not sure

View file

@ -91,6 +91,8 @@ import JumpToBottomButton from "../views/rooms/JumpToBottomButton";
import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar"; import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/SpaceStore";
import { logger } from "matrix-js-sdk/src/logger";
const DEBUG = false; const DEBUG = false;
let debuglog = function(msg: string) {}; let debuglog = function(msg: string) {};
@ -98,7 +100,7 @@ const BROWSER_SUPPORTS_SANDBOX = 'sandbox' in document.createElement('iframe');
if (DEBUG) { if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console // using bind means that we get to keep useful line numbers in the console
debuglog = console.log.bind(console); debuglog = logger.log.bind(console);
} }
interface IProps { interface IProps {
@ -380,7 +382,7 @@ export default class RoomView extends React.Component<IProps, IState> {
} }
// Temporary logging to diagnose https://github.com/vector-im/element-web/issues/4307 // Temporary logging to diagnose https://github.com/vector-im/element-web/issues/4307
console.log( logger.log(
'RVS update:', 'RVS update:',
newState.roomId, newState.roomId,
newState.roomAlias, newState.roomAlias,
@ -1399,7 +1401,7 @@ export default class RoomView extends React.Component<IProps, IState> {
// As per the spec, an all rooms search can create this condition, // As per the spec, an all rooms search can create this condition,
// it happens with Seshat but not Synapse. // it happens with Seshat but not Synapse.
// It will make the result count not match the displayed count. // It will make the result count not match the displayed count.
console.log("Hiding search result from an unknown room", roomId); logger.log("Hiding search result from an unknown room", roomId);
continue; continue;
} }

View file

@ -21,6 +21,8 @@ import { replaceableComponent } from "../../utils/replaceableComponent";
import { getKeyBindingsManager, RoomAction } from "../../KeyBindingsManager"; import { getKeyBindingsManager, RoomAction } from "../../KeyBindingsManager";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import { logger } from "matrix-js-sdk/src/logger";
const DEBUG_SCROLL = false; const DEBUG_SCROLL = false;
// The amount of extra scroll distance to allow prior to unfilling. // The amount of extra scroll distance to allow prior to unfilling.
@ -38,7 +40,7 @@ const PAGE_SIZE = 400;
let debuglog; let debuglog;
if (DEBUG_SCROLL) { if (DEBUG_SCROLL) {
// using bind means that we get to keep useful line numbers in the console // using bind means that we get to keep useful line numbers in the console
debuglog = console.log.bind(console, "ScrollPanel debuglog:"); debuglog = logger.log.bind(console, "ScrollPanel debuglog:");
} else { } else {
debuglog = function() {}; debuglog = function() {};
} }

View file

@ -80,6 +80,8 @@ import Spinner from "../views/elements/Spinner";
import GroupAvatar from "../views/avatars/GroupAvatar"; import GroupAvatar from "../views/avatars/GroupAvatar";
import { useDispatcher } from "../../hooks/useDispatcher"; import { useDispatcher } from "../../hooks/useDispatcher";
import { logger } from "matrix-js-sdk/src/logger";
interface IProps { interface IProps {
space: Room; space: Room;
justCreatedOpts?: IOpts; justCreatedOpts?: IOpts;
@ -696,7 +698,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === "error"); const failedUsers = Object.keys(result.states).filter(a => result.states[a] === "error");
if (failedUsers.length > 0) { if (failedUsers.length > 0) {
console.log("Failed to invite users to space: ", result); logger.log("Failed to invite users to space: ", result);
setError(_t("Failed to invite the following users to your space: %(csvUsers)s", { setError(_t("Failed to invite the following users to your space: %(csvUsers)s", {
csvUsers: failedUsers.join(", "), csvUsers: failedUsers.join(", "),
})); }));

View file

@ -133,15 +133,22 @@ export default class ThreadView extends React.Component<IProps, IState> {
{ this.state.thread && ( { this.state.thread && (
<TimelinePanel <TimelinePanel
ref={this.timelinePanelRef} ref={this.timelinePanelRef}
manageReadReceipts={false} showReadReceipts={false} // No RR support in thread's MVP
manageReadMarkers={false} manageReadReceipts={false} // No RR support in thread's MVP
manageReadMarkers={false} // No RM support in thread's MVP
sendReadReceiptOnLoad={false} // No RR support in thread's MVP
timelineSet={this.state?.thread?.timelineSet} timelineSet={this.state?.thread?.timelineSet}
showUrlPreview={false} showUrlPreview={true}
tileShape={TileShape.Notif} tileShape={TileShape.Notif}
empty={<div>empty</div>} empty={<div>empty</div>}
alwaysShowTimestamps={true} alwaysShowTimestamps={true}
layout={Layout.Group} layout={Layout.Group}
hideThreadedMessages={false} hideThreadedMessages={false}
hidden={false}
showReactions={true}
className="mx_RoomView_messagePanel mx_GroupLayout"
permalinkCreator={this.props.permalinkCreator}
membersLoaded={true}
/> />
) } ) }
<MessageComposer <MessageComposer

View file

@ -49,6 +49,8 @@ import EditorStateTransfer from '../../utils/EditorStateTransfer';
import ErrorDialog from '../views/dialogs/ErrorDialog'; import ErrorDialog from '../views/dialogs/ErrorDialog';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { logger } from "matrix-js-sdk/src/logger";
const PAGINATE_SIZE = 20; const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20; const INITIAL_SIZE = 20;
const READ_RECEIPT_INTERVAL_MS = 500; const READ_RECEIPT_INTERVAL_MS = 500;
@ -60,7 +62,7 @@ const DEBUG = false;
let debuglog = function(...s: any[]) {}; let debuglog = function(...s: any[]) {};
if (DEBUG) { if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console // using bind means that we get to keep useful line numbers in the console
debuglog = console.log.bind(console); debuglog = logger.log.bind(console);
} }
interface IProps { interface IProps {
@ -316,7 +318,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
const differentEventId = newProps.eventId != this.props.eventId; const differentEventId = newProps.eventId != this.props.eventId;
const differentHighlightedEventId = newProps.highlightedEventId != this.props.highlightedEventId; const differentHighlightedEventId = newProps.highlightedEventId != this.props.highlightedEventId;
if (differentEventId || differentHighlightedEventId) { if (differentEventId || differentHighlightedEventId) {
console.log("TimelinePanel switching to eventId " + newProps.eventId + logger.log("TimelinePanel switching to eventId " + newProps.eventId +
" (was " + this.props.eventId + ")"); " (was " + this.props.eventId + ")");
return this.initTimeline(newProps); return this.initTimeline(newProps);
} }
@ -1098,7 +1100,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
// we're in a setState callback, and we know // we're in a setState callback, and we know
// timelineLoading is now false, so render() should have // timelineLoading is now false, so render() should have
// mounted the message panel. // mounted the message panel.
console.log("can't initialise scroll state because " + logger.log("can't initialise scroll state because " +
"messagePanel didn't load"); "messagePanel didn't load");
return; return;
} }

View file

@ -59,6 +59,7 @@ import RoomName from "../views/elements/RoomName";
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import InlineSpinner from "../views/elements/InlineSpinner"; import InlineSpinner from "../views/elements/InlineSpinner";
import TooltipButton from "../views/elements/TooltipButton"; import TooltipButton from "../views/elements/TooltipButton";
import { logger } from "matrix-js-sdk/src/logger";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
} }
@ -239,7 +240,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
// TODO: Archived room view: https://github.com/vector-im/element-web/issues/14038 // TODO: Archived room view: https://github.com/vector-im/element-web/issues/14038
// Note: You'll need to uncomment the button too. // Note: You'll need to uncomment the button too.
console.log("TODO: Show archived rooms"); logger.log("TODO: Show archived rooms");
}; };
private onProvideFeedback = (ev: ButtonEvent) => { private onProvideFeedback = (ev: ButtonEvent) => {

View file

@ -86,8 +86,8 @@ export default class UserView extends React.Component<IProps, IState> {
public render(): JSX.Element { public render(): JSX.Element {
if (this.state.loading) { if (this.state.loading) {
return <Spinner />; return <Spinner />;
} else if (this.state.member?.user) { } else if (this.state.member) {
const panel = <RightPanel user={this.state.member.user} resizeNotifier={this.props.resizeNotifier} />; const panel = <RightPanel member={this.state.member} resizeNotifier={this.props.resizeNotifier} />;
return (<MainSplit panel={panel} resizeNotifier={this.props.resizeNotifier}> return (<MainSplit panel={panel} resizeNotifier={this.props.resizeNotifier}>
<HomePage /> <HomePage />
</MainSplit>); </MainSplit>);

View file

@ -38,6 +38,8 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import AuthBody from "../../views/auth/AuthBody"; import AuthBody from "../../views/auth/AuthBody";
import AuthHeader from "../../views/auth/AuthHeader"; import AuthHeader from "../../views/auth/AuthHeader";
import { logger } from "matrix-js-sdk/src/logger";
// These are used in several places, and come from the js-sdk's autodiscovery // These are used in several places, and come from the js-sdk's autodiscovery
// stuff. We define them here so that they'll be picked up by i18n. // stuff. We define them here so that they'll be picked up by i18n.
_td("Invalid homeserver discovery response"); _td("Invalid homeserver discovery response");
@ -438,7 +440,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
// technically the flow can have multiple steps, but no one does this // technically the flow can have multiple steps, but no one does this
// for login and loginLogic doesn't support it so we can ignore it. // for login and loginLogic doesn't support it so we can ignore it.
if (!this.stepRendererMap[flow.type]) { if (!this.stepRendererMap[flow.type]) {
console.log("Skipping flow", flow, "due to unsupported login type", flow.type); logger.log("Skipping flow", flow, "due to unsupported login type", flow.type);
return false; return false;
} }
return true; return true;

View file

@ -37,6 +37,8 @@ import AuthHeader from "../../views/auth/AuthHeader";
import InteractiveAuth from "../InteractiveAuth"; import InteractiveAuth from "../InteractiveAuth";
import Spinner from "../../views/elements/Spinner"; import Spinner from "../../views/elements/Spinner";
import { logger } from "matrix-js-sdk/src/logger";
interface IProps { interface IProps {
serverConfig: ValidatedServerConfig; serverConfig: ValidatedServerConfig;
defaultDeviceDisplayName: string; defaultDeviceDisplayName: string;
@ -215,7 +217,7 @@ export default class Registration extends React.Component<IProps, IState> {
if (!this.state.doingUIAuth) { if (!this.state.doingUIAuth) {
await this.makeRegisterRequest(null); await this.makeRegisterRequest(null);
// This should never succeed since we specified no auth object. // This should never succeed since we specified no auth object.
console.log("Expecting 401 from register request but got success!"); logger.log("Expecting 401 from register request but got success!");
} }
} catch (e) { } catch (e) {
if (e.httpStatus === 401) { if (e.httpStatus === 401) {
@ -239,7 +241,7 @@ export default class Registration extends React.Component<IProps, IState> {
}); });
} }
} else { } else {
console.log("Unable to query for supported registration methods.", e); logger.log("Unable to query for supported registration methods.", e);
showGenericError(e); showGenericError(e);
} }
} }
@ -330,7 +332,7 @@ export default class Registration extends React.Component<IProps, IState> {
// the user had a separate guest session they didn't actually mean to replace. // the user had a separate guest session they didn't actually mean to replace.
const [sessionOwner, sessionIsGuest] = await Lifecycle.getStoredSessionOwner(); const [sessionOwner, sessionIsGuest] = await Lifecycle.getStoredSessionOwner();
if (sessionOwner && !sessionIsGuest && sessionOwner !== response.userId) { if (sessionOwner && !sessionIsGuest && sessionOwner !== response.userId) {
console.log( logger.log(
`Found a session for ${sessionOwner} but ${response.userId} has just registered.`, `Found a session for ${sessionOwner} but ${response.userId} has just registered.`,
); );
newState.differentLoggedInUserId = sessionOwner; newState.differentLoggedInUserId = sessionOwner;
@ -366,7 +368,7 @@ export default class Registration extends React.Component<IProps, IState> {
const emailPusher = pushers[i]; const emailPusher = pushers[i];
emailPusher.data = { brand: this.props.brand }; emailPusher.data = { brand: this.props.brand };
matrixClient.setPusher(emailPusher).then(() => { matrixClient.setPusher(emailPusher).then(() => {
console.log("Set email branding to " + this.props.brand); logger.log("Set email branding to " + this.props.brand);
}, (error) => { }, (error) => {
console.error("Couldn't set email branding: " + error); console.error("Couldn't set email branding: " + error);
}); });

View file

@ -28,6 +28,8 @@ import Spinner from '../../views/elements/Spinner';
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { logger } from "matrix-js-sdk/src/logger";
function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean { function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean {
return Boolean( return Boolean(
keyInfo.passphrase && keyInfo.passphrase &&
@ -231,7 +233,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
} else if (phase === Phase.Busy || phase === Phase.Loading) { } else if (phase === Phase.Busy || phase === Phase.Loading) {
return <Spinner />; return <Spinner />;
} else { } else {
console.log(`SetupEncryptionBody: Unknown phase ${phase}`); logger.log(`SetupEncryptionBody: Unknown phase ${phase}`);
} }
} }
} }

View file

@ -32,6 +32,8 @@ import Spinner from "../../views/elements/Spinner";
import AuthHeader from "../../views/auth/AuthHeader"; import AuthHeader from "../../views/auth/AuthHeader";
import AuthBody from "../../views/auth/AuthBody"; import AuthBody from "../../views/auth/AuthBody";
import { logger } from "matrix-js-sdk/src/logger";
const LOGIN_VIEW = { const LOGIN_VIEW = {
LOADING: 1, LOADING: 1,
PASSWORD: 2, PASSWORD: 2,
@ -103,7 +105,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
onFinished: (wipeData) => { onFinished: (wipeData) => {
if (!wipeData) return; if (!wipeData) return;
console.log("Clearing data from soft-logged-out session"); logger.log("Clearing data from soft-logged-out session");
Lifecycle.logout(); Lifecycle.logout();
}, },
}); });

View file

@ -19,6 +19,8 @@ import { _t } from '../../../languageHandler';
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { logger } from "matrix-js-sdk/src/logger";
const DIV_ID = 'mx_recaptcha'; const DIV_ID = 'mx_recaptcha';
interface ICaptchaFormProps { interface ICaptchaFormProps {
@ -60,7 +62,7 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
// already loaded // already loaded
this.onCaptchaLoaded(); this.onCaptchaLoaded();
} else { } else {
console.log("Loading recaptcha script..."); logger.log("Loading recaptcha script...");
window.mxOnRecaptchaLoaded = () => { this.onCaptchaLoaded(); }; window.mxOnRecaptchaLoaded = () => { this.onCaptchaLoaded(); };
const scriptTag = document.createElement('script'); const scriptTag = document.createElement('script');
scriptTag.setAttribute( scriptTag.setAttribute(
@ -109,7 +111,7 @@ export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICap
} }
private onCaptchaLoaded() { private onCaptchaLoaded() {
console.log("Loaded recaptcha script."); logger.log("Loaded recaptcha script.");
try { try {
this.renderRecaptcha(DIV_ID); this.renderRecaptcha(DIV_ID);
// clear error if re-rendered // clear error if re-rendered

View file

@ -29,6 +29,8 @@ import { LocalisedPolicy, Policies } from '../../../Terms';
import Field from '../elements/Field'; import Field from '../elements/Field';
import CaptchaForm from "./CaptchaForm"; import CaptchaForm from "./CaptchaForm";
import { logger } from "matrix-js-sdk/src/logger";
/* This file contains a collection of components which are used by the /* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed * InteractiveAuth to prompt the user to enter the information needed
* for an auth stage. (The intention is that they could also be used for other * for an auth stage. (The intention is that they could also be used for other
@ -555,7 +557,7 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
} }
} catch (e) { } catch (e) {
this.props.fail(e); this.props.fail(e);
console.log("Failed to submit msisdn token"); logger.log("Failed to submit msisdn token");
} }
}; };

View file

@ -18,15 +18,54 @@ limitations under the License.
import React from 'react'; import React from 'react';
import FocusLock from 'react-focus-lock'; import FocusLock from 'react-focus-lock';
import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { Key } from '../../../Keyboard'; import { Key } from '../../../Keyboard';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { IDialogProps } from "./IDialogProps";
interface IProps extends IDialogProps {
// Whether the dialog should have a 'close' button that will
// cause the dialog to be cancelled. This should only be set
// to false if there is nothing the app can sensibly do if the
// dialog is cancelled, eg. "We can't restore your session and
// the app cannot work". Default: true.
hasCancel?: boolean;
// called when a key is pressed
onKeyDown?: (e: KeyboardEvent | React.KeyboardEvent) => void;
// CSS class to apply to dialog div
className?: string;
// if true, dialog container is 60% of the viewport width. Otherwise,
// the container will have no fixed size, allowing its contents to
// determine its size. Default: true.
fixedWidth?: boolean;
// Title for the dialog.
title?: JSX.Element | string;
// Path to an icon to put in the header
headerImage?: string;
// children should be the content of the dialog
children?: React.ReactNode;
// Id of content element
// If provided, this is used to add a aria-describedby attribute
contentId?: string;
// optional additional class for the title element (basically anything that can be passed to classnames)
titleClass?: string | string[];
headerButton?: JSX.Element;
}
/* /*
* Basic container for modal dialogs. * Basic container for modal dialogs.
@ -35,54 +74,10 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
* dialog on escape. * dialog on escape.
*/ */
@replaceableComponent("views.dialogs.BaseDialog") @replaceableComponent("views.dialogs.BaseDialog")
export default class BaseDialog extends React.Component { export default class BaseDialog extends React.Component<IProps> {
static propTypes = { private matrixClient: MatrixClient;
// onFinished callback to call when Escape is pressed
// Take a boolean which is true if the dialog was dismissed
// with a positive / confirm action or false if it was
// cancelled (BaseDialog itself only calls this with false).
onFinished: PropTypes.func.isRequired,
// Whether the dialog should have a 'close' button that will public static defaultProps = {
// cause the dialog to be cancelled. This should only be set
// to false if there is nothing the app can sensibly do if the
// dialog is cancelled, eg. "We can't restore your session and
// the app cannot work". Default: true.
hasCancel: PropTypes.bool,
// called when a key is pressed
onKeyDown: PropTypes.func,
// CSS class to apply to dialog div
className: PropTypes.string,
// if true, dialog container is 60% of the viewport width. Otherwise,
// the container will have no fixed size, allowing its contents to
// determine its size. Default: true.
fixedWidth: PropTypes.bool,
// Title for the dialog.
title: PropTypes.node.isRequired,
// Path to an icon to put in the header
headerImage: PropTypes.string,
// children should be the content of the dialog
children: PropTypes.node,
// Id of content element
// If provided, this is used to add a aria-describedby attribute
contentId: PropTypes.string,
// optional additional class for the title element (basically anything that can be passed to classnames)
titleClass: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
PropTypes.arrayOf(PropTypes.string),
]),
};
static defaultProps = {
hasCancel: true, hasCancel: true,
fixedWidth: true, fixedWidth: true,
}; };
@ -90,10 +85,10 @@ export default class BaseDialog extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this._matrixClient = MatrixClientPeg.get(); this.matrixClient = MatrixClientPeg.get();
} }
_onKeyDown = (e) => { private onKeyDown = (e: KeyboardEvent | React.KeyboardEvent): void => {
if (this.props.onKeyDown) { if (this.props.onKeyDown) {
this.props.onKeyDown(e); this.props.onKeyDown(e);
} }
@ -104,15 +99,15 @@ export default class BaseDialog extends React.Component {
} }
}; };
_onCancelClick = (e) => { private onCancelClick = (e: ButtonEvent): void => {
this.props.onFinished(false); this.props.onFinished(false);
}; };
render() { public render(): JSX.Element {
let cancelButton; let cancelButton;
if (this.props.hasCancel) { if (this.props.hasCancel) {
cancelButton = ( cancelButton = (
<AccessibleButton onClick={this._onCancelClick} className="mx_Dialog_cancelButton" aria-label={_t("Close dialog")} /> <AccessibleButton onClick={this.onCancelClick} className="mx_Dialog_cancelButton" aria-label={_t("Close dialog")} />
); );
} }
@ -122,11 +117,11 @@ export default class BaseDialog extends React.Component {
} }
return ( return (
<MatrixClientContext.Provider value={this._matrixClient}> <MatrixClientContext.Provider value={this.matrixClient}>
<FocusLock <FocusLock
returnFocus={true} returnFocus={true}
lockProps={{ lockProps={{
onKeyDown: this._onKeyDown, onKeyDown: this.onKeyDown,
role: "dialog", role: "dialog",
["aria-labelledby"]: "mx_BaseDialog_title", ["aria-labelledby"]: "mx_BaseDialog_title",
// This should point to a node describing the dialog. // This should point to a node describing the dialog.

View file

@ -125,14 +125,14 @@ const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, g
setBusy(true); setBusy(true);
// require & validate the space name field // require & validate the space name field
if (!await spaceNameField.current.validate({ allowEmpty: false })) { if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
setBusy(false); setBusy(false);
spaceNameField.current.focus(); spaceNameField.current.focus();
spaceNameField.current.validate({ allowEmpty: false, focused: true }); spaceNameField.current.validate({ allowEmpty: false, focused: true });
return; return;
} }
// validate the space name alias field but do not require it // validate the space name alias field but do not require it
if (joinRule === JoinRule.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) { if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) {
setBusy(false); setBusy(false);
spaceAliasField.current.focus(); spaceAliasField.current.focus();
spaceAliasField.current.validate({ allowEmpty: true, focused: true }); spaceAliasField.current.validate({ allowEmpty: true, focused: true });

View file

@ -64,14 +64,14 @@ const CreateSubspaceDialog: React.FC<IProps> = ({ space, onAddExistingSpaceClick
setBusy(true); setBusy(true);
// require & validate the space name field // require & validate the space name field
if (!await spaceNameField.current.validate({ allowEmpty: false })) { if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
spaceNameField.current.focus(); spaceNameField.current.focus();
spaceNameField.current.validate({ allowEmpty: false, focused: true }); spaceNameField.current.validate({ allowEmpty: false, focused: true });
setBusy(false); setBusy(false);
return; return;
} }
// validate the space name alias field but do not require it // validate the space name alias field but do not require it
if (joinRule === JoinRule.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) { if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) {
spaceAliasField.current.focus(); spaceAliasField.current.focus();
spaceAliasField.current.validate({ allowEmpty: true, focused: true }); spaceAliasField.current.validate({ allowEmpty: true, focused: true });
setBusy(false); setBusy(false);

View file

@ -23,10 +23,9 @@ import Modal from '../../../Modal';
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons"; import DialogButtons from "../elements/DialogButtons";
import QuestionDialog from "./QuestionDialog"; import QuestionDialog from "./QuestionDialog";
import { IDialogProps } from "./IDialogProps";
interface IProps { interface IProps extends IDialogProps {}
onFinished: (success: boolean) => void;
}
const CryptoStoreTooNewDialog: React.FC<IProps> = (props: IProps) => { const CryptoStoreTooNewDialog: React.FC<IProps> = (props: IProps) => {
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;

View file

@ -44,6 +44,8 @@ import { SettingLevel } from '../../../settings/SettingLevel';
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import TruncatedList from "../elements/TruncatedList"; import TruncatedList from "../elements/TruncatedList";
import { logger } from "matrix-js-sdk/src/logger";
interface IGenericEditorProps { interface IGenericEditorProps {
onBack: () => void; onBack: () => void;
} }
@ -984,7 +986,7 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
const parsedExplicit = JSON.parse(this.state.explicitValues); const parsedExplicit = JSON.parse(this.state.explicitValues);
const parsedExplicitRoom = JSON.parse(this.state.explicitRoomValues); const parsedExplicitRoom = JSON.parse(this.state.explicitRoomValues);
for (const level of Object.keys(parsedExplicit)) { for (const level of Object.keys(parsedExplicit)) {
console.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`); logger.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`);
try { try {
const val = parsedExplicit[level]; const val = parsedExplicit[level];
await SettingsStore.setValue(settingId, null, level as SettingLevel, val); await SettingsStore.setValue(settingId, null, level as SettingLevel, val);
@ -994,7 +996,7 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
} }
const roomId = this.props.room.roomId; const roomId = this.props.room.roomId;
for (const level of Object.keys(parsedExplicit)) { for (const level of Object.keys(parsedExplicit)) {
console.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`); logger.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`);
try { try {
const val = parsedExplicitRoom[level]; const val = parsedExplicitRoom[level];
await SettingsStore.setValue(settingId, roomId, level as SettingLevel, val); await SettingsStore.setValue(settingId, roomId, level as SettingLevel, val);

View file

@ -19,30 +19,33 @@ import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Field from "../elements/Field"; import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics, { Rating } from "../../../CountlyAnalytics";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import BugReportDialog from "./BugReportDialog"; import BugReportDialog from "./BugReportDialog";
import InfoDialog from "./InfoDialog"; import InfoDialog from "./InfoDialog";
import StyledRadioGroup from "../elements/StyledRadioGroup"; import StyledRadioGroup from "../elements/StyledRadioGroup";
import { IDialogProps } from "./IDialogProps";
const existingIssuesUrl = "https://github.com/vector-im/element-web/issues" + const existingIssuesUrl = "https://github.com/vector-im/element-web/issues" +
"?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc"; "?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc";
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new/choose"; const newIssueUrl = "https://github.com/vector-im/element-web/issues/new/choose";
export default (props) => { interface IProps extends IDialogProps {}
const [rating, setRating] = useState("");
const [comment, setComment] = useState("");
const onDebugLogsLinkClick = () => { const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
const [rating, setRating] = useState<Rating>();
const [comment, setComment] = useState<string>("");
const onDebugLogsLinkClick = (): void => {
props.onFinished(); props.onFinished();
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {}); Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
}; };
const hasFeedback = CountlyAnalytics.instance.canEnable(); const hasFeedback = CountlyAnalytics.instance.canEnable();
const onFinished = (sendFeedback) => { const onFinished = (sendFeedback: boolean): void => {
if (hasFeedback && sendFeedback) { if (hasFeedback && sendFeedback) {
CountlyAnalytics.instance.reportFeedback(parseInt(rating, 10), comment); CountlyAnalytics.instance.reportFeedback(rating, comment);
Modal.createTrackedDialog('Feedback sent', '', InfoDialog, { Modal.createTrackedDialog('Feedback sent', '', InfoDialog, {
title: _t('Feedback sent'), title: _t('Feedback sent'),
description: _t('Thank you!'), description: _t('Thank you!'),
@ -65,8 +68,8 @@ export default (props) => {
<StyledRadioGroup <StyledRadioGroup
name="feedbackRating" name="feedbackRating"
value={rating} value={String(rating)}
onChange={setRating} onChange={(r) => setRating(parseInt(r, 10) as Rating)}
definitions={[ definitions={[
{ value: "1", label: "😠" }, { value: "1", label: "😠" },
{ value: "2", label: "😞" }, { value: "2", label: "😞" },
@ -138,7 +141,9 @@ export default (props) => {
{ countlyFeedbackSection } { countlyFeedbackSection }
</React.Fragment>} </React.Fragment>}
button={hasFeedback ? _t("Send feedback") : _t("Go back")} button={hasFeedback ? _t("Send feedback") : _t("Go back")}
buttonDisabled={hasFeedback && rating === ""} buttonDisabled={hasFeedback && !rating}
onFinished={onFinished} onFinished={onFinished}
/>); />);
}; };
export default FeedbackDialog;

View file

@ -15,12 +15,22 @@ 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 { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media"; import { mediaFromMxc } from "../../../customisations/Media";
import VerificationComplete from "../verification/VerificationComplete";
import VerificationCancelled from "../verification/VerificationCancelled";
import BaseAvatar from "../avatars/BaseAvatar";
import Spinner from "../elements/Spinner";
import VerificationShowSas from "../verification/VerificationShowSas";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import { IDialogProps } from "./IDialogProps";
import { IGeneratedSas, ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
import { VerificationBase } from "matrix-js-sdk/src/crypto/verification/Base";
import { logger } from "matrix-js-sdk/src/logger";
const PHASE_START = 0; const PHASE_START = 0;
const PHASE_SHOW_SAS = 1; const PHASE_SHOW_SAS = 1;
@ -28,41 +38,56 @@ const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 2;
const PHASE_VERIFIED = 3; const PHASE_VERIFIED = 3;
const PHASE_CANCELLED = 4; const PHASE_CANCELLED = 4;
@replaceableComponent("views.dialogs.IncomingSasDialog") interface IProps extends IDialogProps {
export default class IncomingSasDialog extends React.Component { verifier: VerificationBase; // TODO types
static propTypes = { }
verifier: PropTypes.object.isRequired,
};
constructor(props) { interface IState {
phase: number;
sasVerified: boolean;
opponentProfile: {
// eslint-disable-next-line camelcase
avatar_url?: string;
displayname?: string;
};
opponentProfileError: Error;
sas: IGeneratedSas;
}
@replaceableComponent("views.dialogs.IncomingSasDialog")
export default class IncomingSasDialog extends React.Component<IProps, IState> {
private showSasEvent: ISasEvent;
constructor(props: IProps) {
super(props); super(props);
let phase = PHASE_START; let phase = PHASE_START;
if (this.props.verifier.cancelled) { if (this.props.verifier.hasBeenCancelled) {
console.log("Verifier was cancelled in the background."); logger.log("Verifier was cancelled in the background.");
phase = PHASE_CANCELLED; phase = PHASE_CANCELLED;
} }
this._showSasEvent = null; this.showSasEvent = null;
this.state = { this.state = {
phase: phase, phase: phase,
sasVerified: false, sasVerified: false,
opponentProfile: null, opponentProfile: null,
opponentProfileError: null, opponentProfileError: null,
sas: null,
}; };
this.props.verifier.on('show_sas', this._onVerifierShowSas); this.props.verifier.on('show_sas', this.onVerifierShowSas);
this.props.verifier.on('cancel', this._onVerifierCancel); this.props.verifier.on('cancel', this.onVerifierCancel);
this._fetchOpponentProfile(); this.fetchOpponentProfile();
} }
componentWillUnmount() { public componentWillUnmount(): void {
if (this.state.phase !== PHASE_CANCELLED && this.state.phase !== PHASE_VERIFIED) { if (this.state.phase !== PHASE_CANCELLED && this.state.phase !== PHASE_VERIFIED) {
this.props.verifier.cancel('User cancel'); this.props.verifier.cancel(new Error('User cancel'));
} }
this.props.verifier.removeListener('show_sas', this._onVerifierShowSas); this.props.verifier.removeListener('show_sas', this.onVerifierShowSas);
} }
async _fetchOpponentProfile() { private async fetchOpponentProfile(): Promise<void> {
try { try {
const prof = await MatrixClientPeg.get().getProfileInfo( const prof = await MatrixClientPeg.get().getProfileInfo(
this.props.verifier.userId, this.props.verifier.userId,
@ -77,53 +102,49 @@ export default class IncomingSasDialog extends React.Component {
} }
} }
_onFinished = () => { private onFinished = (): void => {
this.props.onFinished(this.state.phase === PHASE_VERIFIED); this.props.onFinished(this.state.phase === PHASE_VERIFIED);
} };
_onCancelClick = () => { private onCancelClick = (): void => {
this.props.onFinished(this.state.phase === PHASE_VERIFIED); this.props.onFinished(this.state.phase === PHASE_VERIFIED);
} };
_onContinueClick = () => { private onContinueClick = (): void => {
this.setState({ phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM }); this.setState({ phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM });
this.props.verifier.verify().then(() => { this.props.verifier.verify().then(() => {
this.setState({ phase: PHASE_VERIFIED }); this.setState({ phase: PHASE_VERIFIED });
}).catch((e) => { }).catch((e) => {
console.log("Verification failed", e); logger.log("Verification failed", e);
}); });
} };
_onVerifierShowSas = (e) => { private onVerifierShowSas = (e: ISasEvent): void => {
this._showSasEvent = e; this.showSasEvent = e;
this.setState({ this.setState({
phase: PHASE_SHOW_SAS, phase: PHASE_SHOW_SAS,
sas: e.sas, sas: e.sas,
}); });
} };
_onVerifierCancel = (e) => { private onVerifierCancel = (): void => {
this.setState({ this.setState({
phase: PHASE_CANCELLED, phase: PHASE_CANCELLED,
}); });
} };
_onSasMatchesClick = () => { private onSasMatchesClick = (): void => {
this._showSasEvent.confirm(); this.showSasEvent.confirm();
this.setState({ this.setState({
phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM, phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM,
}); });
} };
_onVerifiedDoneClick = () => { private onVerifiedDoneClick = (): void => {
this.props.onFinished(true); this.props.onFinished(true);
} };
_renderPhaseStart() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Spinner = sdk.getComponent("views.elements.Spinner");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
private renderPhaseStart(): JSX.Element {
const isSelf = this.props.verifier.userId === MatrixClientPeg.get().getUserId(); const isSelf = this.props.verifier.userId === MatrixClientPeg.get().getUserId();
let profile; let profile;
@ -190,27 +211,24 @@ export default class IncomingSasDialog extends React.Component {
<DialogButtons <DialogButtons
primaryButton={_t('Continue')} primaryButton={_t('Continue')}
hasCancel={true} hasCancel={true}
onPrimaryButtonClick={this._onContinueClick} onPrimaryButtonClick={this.onContinueClick}
onCancel={this._onCancelClick} onCancel={this.onCancelClick}
/> />
</div> </div>
); );
} }
_renderPhaseShowSas() { private renderPhaseShowSas(): JSX.Element {
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
return <VerificationShowSas return <VerificationShowSas
sas={this._showSasEvent.sas} sas={this.showSasEvent.sas}
onCancel={this._onCancelClick} onCancel={this.onCancelClick}
onDone={this._onSasMatchesClick} onDone={this.onSasMatchesClick}
isSelf={this.props.verifier.userId === MatrixClientPeg.get().getUserId()} isSelf={this.props.verifier.userId === MatrixClientPeg.get().getUserId()}
inDialog={true} inDialog={true}
/>; />;
} }
_renderPhaseWaitForPartnerToConfirm() { private renderPhaseWaitForPartnerToConfirm(): JSX.Element {
const Spinner = sdk.getComponent("views.elements.Spinner");
return ( return (
<div> <div>
<Spinner /> <Spinner />
@ -219,41 +237,38 @@ export default class IncomingSasDialog extends React.Component {
); );
} }
_renderPhaseVerified() { private renderPhaseVerified(): JSX.Element {
const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete'); return <VerificationComplete onDone={this.onVerifiedDoneClick} />;
return <VerificationComplete onDone={this._onVerifiedDoneClick} />;
} }
_renderPhaseCancelled() { private renderPhaseCancelled(): JSX.Element {
const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled'); return <VerificationCancelled onDone={this.onCancelClick} />;
return <VerificationCancelled onDone={this._onCancelClick} />;
} }
render() { public render(): JSX.Element {
let body; let body;
switch (this.state.phase) { switch (this.state.phase) {
case PHASE_START: case PHASE_START:
body = this._renderPhaseStart(); body = this.renderPhaseStart();
break; break;
case PHASE_SHOW_SAS: case PHASE_SHOW_SAS:
body = this._renderPhaseShowSas(); body = this.renderPhaseShowSas();
break; break;
case PHASE_WAIT_FOR_PARTNER_TO_CONFIRM: case PHASE_WAIT_FOR_PARTNER_TO_CONFIRM:
body = this._renderPhaseWaitForPartnerToConfirm(); body = this.renderPhaseWaitForPartnerToConfirm();
break; break;
case PHASE_VERIFIED: case PHASE_VERIFIED:
body = this._renderPhaseVerified(); body = this.renderPhaseVerified();
break; break;
case PHASE_CANCELLED: case PHASE_CANCELLED:
body = this._renderPhaseCancelled(); body = this.renderPhaseCancelled();
break; break;
} }
const BaseDialog = sdk.getComponent("dialogs.BaseDialog");
return ( return (
<BaseDialog <BaseDialog
title={_t("Incoming Verification Request")} title={_t("Incoming Verification Request")}
onFinished={this._onFinished} onFinished={this.onFinished}
fixedWidth={false} fixedWidth={false}
> >
{ body } { body }

View file

@ -15,32 +15,28 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import * as sdk from "../../../index";
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import { IDialogProps } from "./IDialogProps";
interface IProps extends IDialogProps {}
@replaceableComponent("views.dialogs.IntegrationsDisabledDialog") @replaceableComponent("views.dialogs.IntegrationsDisabledDialog")
export default class IntegrationsDisabledDialog extends React.Component { export default class IntegrationsDisabledDialog extends React.Component<IProps> {
static propTypes = { private onAcknowledgeClick = (): void => {
onFinished: PropTypes.func.isRequired,
};
_onAcknowledgeClick = () => {
this.props.onFinished(); this.props.onFinished();
}; };
_onOpenSettingsClick = () => { private onOpenSettingsClick = (): void => {
this.props.onFinished(); this.props.onFinished();
dis.fire(Action.ViewUserSettings); dis.fire(Action.ViewUserSettings);
}; };
render() { public render(): JSX.Element {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return ( return (
<BaseDialog <BaseDialog
className='mx_IntegrationsDisabledDialog' className='mx_IntegrationsDisabledDialog'
@ -53,9 +49,9 @@ export default class IntegrationsDisabledDialog extends React.Component {
</div> </div>
<DialogButtons <DialogButtons
primaryButton={_t("Settings")} primaryButton={_t("Settings")}
onPrimaryButtonClick={this._onOpenSettingsClick} onPrimaryButtonClick={this.onOpenSettingsClick}
cancelButton={_t("OK")} cancelButton={_t("OK")}
onCancel={this._onAcknowledgeClick} onCancel={this.onAcknowledgeClick}
/> />
</BaseDialog> </BaseDialog>
); );

View file

@ -15,23 +15,21 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import * as sdk from "../../../index"; import * as sdk from "../../../index";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IDialogProps } from "./IDialogProps";
interface IProps extends IDialogProps {}
@replaceableComponent("views.dialogs.IntegrationsImpossibleDialog") @replaceableComponent("views.dialogs.IntegrationsImpossibleDialog")
export default class IntegrationsImpossibleDialog extends React.Component { export default class IntegrationsImpossibleDialog extends React.Component<IProps> {
static propTypes = { private onAcknowledgeClick = (): void => {
onFinished: PropTypes.func.isRequired,
};
_onAcknowledgeClick = () => {
this.props.onFinished(); this.props.onFinished();
}; };
render() { public render(): JSX.Element {
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
@ -54,7 +52,7 @@ export default class IntegrationsImpossibleDialog extends React.Component {
</div> </div>
<DialogButtons <DialogButtons
primaryButton={_t("OK")} primaryButton={_t("OK")}
onPrimaryButtonClick={this._onAcknowledgeClick} onPrimaryButtonClick={this.onAcknowledgeClick}
hasCancel={false} hasCancel={false}
/> />
</BaseDialog> </BaseDialog>

View file

@ -17,69 +17,88 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import { ERROR_USER_CANCELLED } from "../../structures/InteractiveAuth"; import InteractiveAuth, { ERROR_USER_CANCELLED } from "../../structures/InteractiveAuth";
import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents"; import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MatrixClient } from "matrix-js-sdk/src/client";
import BaseDialog from "./BaseDialog";
import { IAuthData } from "matrix-js-sdk/src/interactive-auth";
import { IDialogProps } from "./IDialogProps";
interface IDialogAesthetics {
[x: string]: {
[x: number]: {
title: string;
body: string;
continueText: string;
continueKind: string;
};
};
}
interface IProps extends IDialogProps {
// matrix client to use for UI auth requests
matrixClient: MatrixClient;
// response from initial request. If not supplied, will do a request on
// mount.
authData?: IAuthData;
// callback
makeRequest: (auth: IAuthData) => Promise<IAuthData>;
// Optional title and body to show when not showing a particular stage
title?: string;
body?: string;
// Optional title and body pairs for particular stages and phases within
// those stages. Object structure/example is:
// {
// "org.example.stage_type": {
// 1: {
// "body": "This is a body for phase 1" of org.example.stage_type,
// "title": "Title for phase 1 of org.example.stage_type"
// },
// 2: {
// "body": "This is a body for phase 2 of org.example.stage_type",
// "title": "Title for phase 2 of org.example.stage_type"
// "continueText": "Confirm identity with Example Auth",
// "continueKind": "danger"
// }
// }
// }
//
// Default is defined in _getDefaultDialogAesthetics()
aestheticsForStagePhases?: IDialogAesthetics;
}
interface IState {
authError: Error;
// See _onUpdateStagePhase()
uiaStage: number | string;
uiaStagePhase: number | string;
}
@replaceableComponent("views.dialogs.InteractiveAuthDialog") @replaceableComponent("views.dialogs.InteractiveAuthDialog")
export default class InteractiveAuthDialog extends React.Component { export default class InteractiveAuthDialog extends React.Component<IProps, IState> {
static propTypes = { constructor(props: IProps) {
// matrix client to use for UI auth requests super(props);
matrixClient: PropTypes.object.isRequired,
// response from initial request. If not supplied, will do a request on this.state = {
// mount. authError: null,
authData: PropTypes.shape({
flows: PropTypes.array,
params: PropTypes.object,
session: PropTypes.string,
}),
// callback // See _onUpdateStagePhase()
makeRequest: PropTypes.func.isRequired, uiaStage: null,
uiaStagePhase: null,
};
}
onFinished: PropTypes.func.isRequired, private getDefaultDialogAesthetics(): IDialogAesthetics {
// Optional title and body to show when not showing a particular stage
title: PropTypes.string,
body: PropTypes.string,
// Optional title and body pairs for particular stages and phases within
// those stages. Object structure/example is:
// {
// "org.example.stage_type": {
// 1: {
// "body": "This is a body for phase 1" of org.example.stage_type,
// "title": "Title for phase 1 of org.example.stage_type"
// },
// 2: {
// "body": "This is a body for phase 2 of org.example.stage_type",
// "title": "Title for phase 2 of org.example.stage_type"
// "continueText": "Confirm identity with Example Auth",
// "continueKind": "danger"
// }
// }
// }
//
// Default is defined in _getDefaultDialogAesthetics()
aestheticsForStagePhases: PropTypes.object,
};
state = {
authError: null,
// See _onUpdateStagePhase()
uiaStage: null,
uiaStagePhase: null,
};
_getDefaultDialogAesthetics() {
const ssoAesthetics = { const ssoAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: { [SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"), title: _t("Use Single Sign On to continue"),
@ -101,7 +120,7 @@ export default class InteractiveAuthDialog extends React.Component {
}; };
} }
_onAuthFinished = (success, result) => { private onAuthFinished = (success: boolean, result: Error): void => {
if (success) { if (success) {
this.props.onFinished(true, result); this.props.onFinished(true, result);
} else { } else {
@ -115,19 +134,16 @@ export default class InteractiveAuthDialog extends React.Component {
} }
}; };
_onUpdateStagePhase = (newStage, newPhase) => { private onUpdateStagePhase = (newStage: string | number, newPhase: string | number): void => {
// We copy the stage and stage phase params into state for title selection in render() // We copy the stage and stage phase params into state for title selection in render()
this.setState({ uiaStage: newStage, uiaStagePhase: newPhase }); this.setState({ uiaStage: newStage, uiaStagePhase: newPhase });
}; };
_onDismissClick = () => { private onDismissClick = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
}; };
render() { public render(): JSX.Element {
const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
// Let's pick a title, body, and other params text that we'll show to the user. The order // Let's pick a title, body, and other params text that we'll show to the user. The order
// is most specific first, so stagePhase > our props > defaults. // is most specific first, so stagePhase > our props > defaults.
@ -135,7 +151,7 @@ export default class InteractiveAuthDialog extends React.Component {
let body = this.state.authError ? null : this.props.body; let body = this.state.authError ? null : this.props.body;
let continueText = null; let continueText = null;
let continueKind = null; let continueKind = null;
const dialogAesthetics = this.props.aestheticsForStagePhases || this._getDefaultDialogAesthetics(); const dialogAesthetics = this.props.aestheticsForStagePhases || this.getDefaultDialogAesthetics();
if (!this.state.authError && dialogAesthetics) { if (!this.state.authError && dialogAesthetics) {
if (dialogAesthetics[this.state.uiaStage]) { if (dialogAesthetics[this.state.uiaStage]) {
const aesthetics = dialogAesthetics[this.state.uiaStage][this.state.uiaStagePhase]; const aesthetics = dialogAesthetics[this.state.uiaStage][this.state.uiaStagePhase];
@ -152,9 +168,9 @@ export default class InteractiveAuthDialog extends React.Component {
<div id='mx_Dialog_content'> <div id='mx_Dialog_content'>
<div role="alert">{ this.state.authError.message || this.state.authError.toString() }</div> <div role="alert">{ this.state.authError.message || this.state.authError.toString() }</div>
<br /> <br />
<AccessibleButton onClick={this._onDismissClick} <AccessibleButton onClick={this.onDismissClick}
className="mx_GeneralButton" className="mx_GeneralButton"
autoFocus="true" autoFocus={true}
> >
{ _t("Dismiss") } { _t("Dismiss") }
</AccessibleButton> </AccessibleButton>
@ -165,12 +181,11 @@ export default class InteractiveAuthDialog extends React.Component {
<div id='mx_Dialog_content'> <div id='mx_Dialog_content'>
{ body } { body }
<InteractiveAuth <InteractiveAuth
ref={this._collectInteractiveAuth}
matrixClient={this.props.matrixClient} matrixClient={this.props.matrixClient}
authData={this.props.authData} authData={this.props.authData}
makeRequest={this.props.makeRequest} makeRequest={this.props.makeRequest}
onAuthFinished={this._onAuthFinished} onAuthFinished={this.onAuthFinished}
onStagePhaseChange={this._onUpdateStagePhase} onStagePhaseChange={this.onUpdateStagePhase}
continueText={continueText} continueText={continueText}
continueKind={continueKind} continueKind={continueKind}
/> />

View file

@ -73,6 +73,8 @@ import BaseDialog from "./BaseDialog";
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton"; import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/SpaceStore";
import { logger } from "matrix-js-sdk/src/logger";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here. // we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@ -775,7 +777,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
invitedUsers.push(addr); invitedUsers.push(addr);
} }
} }
console.log("Sharing history with", invitedUsers); logger.log("Sharing history with", invitedUsers);
cli.sendSharedHistoryKeys( cli.sendSharedHistoryKeys(
this.props.roomId, invitedUsers, this.props.roomId, invitedUsers,
); );

View file

@ -15,20 +15,29 @@ limitations under the License.
*/ */
import React, { useState, useCallback, useRef } from 'react'; import React, { useState, useCallback, useRef } from 'react';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import Spinner from "../elements/Spinner";
import { IDialogProps } from "./IDialogProps";
export default function KeySignatureUploadFailedDialog({ interface IProps extends IDialogProps {
failures: Record<string, Record<string, {
errcode: string;
error: string;
}>>;
source: string;
continuation: () => void;
}
const KeySignatureUploadFailedDialog: React.FC<IProps> = ({
failures, failures,
source, source,
continuation, continuation,
onFinished, onFinished,
}) { }) => {
const RETRIES = 2; const RETRIES = 2;
const BaseDialog = sdk.getComponent('dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Spinner = sdk.getComponent('elements.Spinner');
const [retry, setRetry] = useState(RETRIES); const [retry, setRetry] = useState(RETRIES);
const [cancelled, setCancelled] = useState(false); const [cancelled, setCancelled] = useState(false);
const [retrying, setRetrying] = useState(false); const [retrying, setRetrying] = useState(false);
@ -107,4 +116,6 @@ export default function KeySignatureUploadFailedDialog({
{ body } { body }
</BaseDialog> </BaseDialog>
); );
} };
export default KeySignatureUploadFailedDialog;

View file

@ -19,8 +19,13 @@ import React from 'react';
import QuestionDialog from './QuestionDialog'; import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import { IDialogProps } from "./IDialogProps";
export default (props) => { interface IProps extends IDialogProps {
host: string;
}
const LazyLoadingDisabledDialog: React.FC<IProps> = (props) => {
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
const description1 = _t( const description1 = _t(
"You've previously used %(brand)s on %(host)s with lazy loading of members enabled. " + "You've previously used %(brand)s on %(host)s with lazy loading of members enabled. " +
@ -49,3 +54,5 @@ export default (props) => {
onFinished={props.onFinished} onFinished={props.onFinished}
/>); />);
}; };
export default LazyLoadingDisabledDialog;

View file

@ -19,8 +19,11 @@ import React from 'react';
import QuestionDialog from './QuestionDialog'; import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import { IDialogProps } from "./IDialogProps";
export default (props) => { interface IProps extends IDialogProps {}
const LazyLoadingResyncDialog: React.FC<IProps> = (props) => {
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
const description = const description =
_t( _t(
@ -38,3 +41,5 @@ export default (props) => {
onFinished={props.onFinished} onFinished={props.onFinished}
/>); />);
}; };
export default LazyLoadingResyncDialog;

View file

@ -66,11 +66,13 @@ const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
> >
<div className="mx_Dialog_content" id="mx_LeaveSpaceDialog"> <div className="mx_Dialog_content" id="mx_LeaveSpaceDialog">
<p> <p>
{ _t("Are you sure you want to leave <spaceName/>?", {}, { { _t("You are about to leave <spaceName/>.", {}, {
spaceName: () => <b>{ space.name }</b>, spaceName: () => <b>{ space.name }</b>,
}) } }) }
&nbsp; &nbsp;
{ rejoinWarning } { rejoinWarning }
{ rejoinWarning && (<>&nbsp;</>) }
{ spaceChildren.length > 0 && _t("Would you like to leave the rooms in this space?") }
</p> </p>
{ spaceChildren.length > 0 && ( { spaceChildren.length > 0 && (
@ -79,9 +81,9 @@ const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
spaceChildren={spaceChildren} spaceChildren={spaceChildren}
selected={selectedRooms} selected={selectedRooms}
onChange={setRoomsToLeave} onChange={setRoomsToLeave}
noneLabel={_t("Don't leave any")} noneLabel={_t("Don't leave any rooms")}
allLabel={_t("Leave all rooms and spaces")} allLabel={_t("Leave all rooms")}
specificLabel={_t("Leave specific rooms and spaces")} specificLabel={_t("Leave some rooms")}
/> />
) } ) }

View file

@ -25,6 +25,8 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog'; import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { logger } from "matrix-js-sdk/src/logger";
interface IProps { interface IProps {
onFinished: (success: boolean) => void; onFinished: (success: boolean) => void;
} }
@ -68,7 +70,7 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
backupInfo, backupInfo,
}); });
} catch (e) { } catch (e) {
console.log("Unable to fetch key backup status", e); logger.log("Unable to fetch key backup status", e);
this.setState({ this.setState({
loading: false, loading: false,
error: e, error: e,

View file

@ -19,37 +19,31 @@ 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 * as FormattingUtils from '../../../utils/FormattingUtils'; import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import QuestionDialog from "./QuestionDialog";
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import { IDialogProps } from "./IDialogProps";
interface IProps extends IDialogProps {
userId: string;
device: DeviceInfo;
}
@replaceableComponent("views.dialogs.ManualDeviceKeyVerificationDialog") @replaceableComponent("views.dialogs.ManualDeviceKeyVerificationDialog")
export default class ManualDeviceKeyVerificationDialog extends React.Component { export default class ManualDeviceKeyVerificationDialog extends React.Component<IProps> {
static propTypes = { private onLegacyFinished = (confirm: boolean): void => {
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
};
_onCancelClick = () => {
this.props.onFinished(false);
}
_onLegacyFinished = (confirm) => {
if (confirm) { if (confirm) {
MatrixClientPeg.get().setDeviceVerified( MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.props.device.deviceId, true, this.props.userId, this.props.device.deviceId, true,
); );
} }
this.props.onFinished(confirm); this.props.onFinished(confirm);
} };
render() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
public render(): JSX.Element {
let text; let text;
if (MatrixClientPeg.get().getUserId() === this.props.userId) { if (MatrixClientPeg.get().getUserId() === this.props.userId) {
text = _t("Confirm by comparing the following with the User Settings in your other session:"); text = _t("Confirm by comparing the following with the User Settings in your other session:");
@ -81,7 +75,7 @@ export default class ManualDeviceKeyVerificationDialog extends React.Component {
title={_t("Verify session")} title={_t("Verify session")}
description={body} description={body}
button={_t("Verify session")} button={_t("Verify session")}
onFinished={this._onLegacyFinished} onFinished={this.onLegacyFinished}
/> />
); );
} }

View file

@ -15,21 +15,39 @@ 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 { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from "../../../index";
import { wantsDateSeparator } from '../../../DateUtils'; import { wantsDateSeparator } from '../../../DateUtils';
import SettingsStore from '../../../settings/SettingsStore'; import SettingsStore from '../../../settings/SettingsStore';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import BaseDialog from "./BaseDialog";
import ScrollPanel from "../../structures/ScrollPanel";
import Spinner from "../elements/Spinner";
import EditHistoryMessage from "../messages/EditHistoryMessage";
import DateSeparator from "../messages/DateSeparator";
import { IDialogProps } from "./IDialogProps";
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
import { defer } from "matrix-js-sdk/src/utils";
interface IProps extends IDialogProps {
mxEvent: MatrixEvent;
}
interface IState {
originalEvent: MatrixEvent;
error: {
errcode: string;
};
events: MatrixEvent[];
nextBatch: string;
isLoading: boolean;
isTwelveHour: boolean;
}
@replaceableComponent("views.dialogs.MessageEditHistoryDialog") @replaceableComponent("views.dialogs.MessageEditHistoryDialog")
export default class MessageEditHistoryDialog extends React.PureComponent { export default class MessageEditHistoryDialog extends React.PureComponent<IProps, IState> {
static propTypes = { constructor(props: IProps) {
mxEvent: PropTypes.object.isRequired,
};
constructor(props) {
super(props); super(props);
this.state = { this.state = {
originalEvent: null, originalEvent: null,
@ -41,7 +59,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
}; };
} }
loadMoreEdits = async (backwards) => { private loadMoreEdits = async (backwards?: boolean): Promise<boolean> => {
if (backwards || (!this.state.nextBatch && !this.state.isLoading)) { if (backwards || (!this.state.nextBatch && !this.state.isLoading)) {
// bail out on backwards as we only paginate in one direction // bail out on backwards as we only paginate in one direction
return false; return false;
@ -50,13 +68,13 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
const roomId = this.props.mxEvent.getRoomId(); const roomId = this.props.mxEvent.getRoomId();
const eventId = this.props.mxEvent.getId(); const eventId = this.props.mxEvent.getId();
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const { resolve, reject, promise } = defer<boolean>();
let result; let result;
let resolve;
let reject;
const promise = new Promise((_resolve, _reject) => {resolve = _resolve; reject = _reject;});
try { try {
result = await client.relations( result = await client.relations(
roomId, eventId, "m.replace", "m.room.message", opts); roomId, eventId, RelationType.Replace, EventType.RoomMessage, opts);
} catch (error) { } catch (error) {
// log if the server returned an error // log if the server returned an error
if (error.errcode) { if (error.errcode) {
@ -67,7 +85,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
} }
const newEvents = result.events; const newEvents = result.events;
this._locallyRedactEventsIfNeeded(newEvents); this.locallyRedactEventsIfNeeded(newEvents);
this.setState({ this.setState({
originalEvent: this.state.originalEvent || result.originalEvent, originalEvent: this.state.originalEvent || result.originalEvent,
events: this.state.events.concat(newEvents), events: this.state.events.concat(newEvents),
@ -78,9 +96,9 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
resolve(hasMoreResults); resolve(hasMoreResults);
}); });
return promise; return promise;
} };
_locallyRedactEventsIfNeeded(newEvents) { private locallyRedactEventsIfNeeded(newEvents: MatrixEvent[]): void {
const roomId = this.props.mxEvent.getRoomId(); const roomId = this.props.mxEvent.getRoomId();
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(roomId); const room = client.getRoom(roomId);
@ -95,13 +113,11 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
} }
} }
componentDidMount() { public componentDidMount(): void {
this.loadMoreEdits(); this.loadMoreEdits();
} }
_renderEdits() { private renderEdits(): JSX.Element[] {
const EditHistoryMessage = sdk.getComponent('messages.EditHistoryMessage');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const nodes = []; const nodes = [];
let lastEvent; let lastEvent;
let allEvents = this.state.events; let allEvents = this.state.events;
@ -128,7 +144,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
return nodes; return nodes;
} }
render() { public render(): JSX.Element {
let content; let content;
if (this.state.error) { if (this.state.error) {
const { error } = this.state; const { error } = this.state;
@ -149,20 +165,17 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
</p>); </p>);
} }
} else if (this.state.isLoading) { } else if (this.state.isLoading) {
const Spinner = sdk.getComponent("elements.Spinner");
content = <Spinner />; content = <Spinner />;
} else { } else {
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
content = (<ScrollPanel content = (<ScrollPanel
className="mx_MessageEditHistoryDialog_scrollPanel" className="mx_MessageEditHistoryDialog_scrollPanel"
onFillRequest={this.loadMoreEdits} onFillRequest={this.loadMoreEdits}
stickyBottom={false} stickyBottom={false}
startAtBottom={false} startAtBottom={false}
> >
<ul className="mx_MessageEditHistoryDialog_edits">{ this._renderEdits() }</ul> <ul className="mx_MessageEditHistoryDialog_edits">{ this.renderEdits() }</ul>
</ScrollPanel>); </ScrollPanel>);
} }
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return ( return (
<BaseDialog <BaseDialog
className='mx_MessageEditHistoryDialog' className='mx_MessageEditHistoryDialog'

View file

@ -16,29 +16,30 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import classNames from "classnames"; import classNames from "classnames";
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { IDialogProps } from "./IDialogProps";
export default class QuestionDialog extends React.Component { interface IProps extends IDialogProps {
static propTypes = { title?: string;
title: PropTypes.string, description?: React.ReactNode;
description: PropTypes.node, extraButtons?: React.ReactNode;
extraButtons: PropTypes.node, button?: string;
button: PropTypes.string, buttonDisabled?: boolean;
buttonDisabled: PropTypes.bool, danger?: boolean;
danger: PropTypes.bool, focus?: boolean;
focus: PropTypes.bool, headerImage?: string;
onFinished: PropTypes.func.isRequired, quitOnly?: boolean; // quitOnly doesn't show the cancel button just the quit [x].
headerImage: PropTypes.string, fixedWidth?: boolean;
quitOnly: PropTypes.bool, // quitOnly doesn't show the cancel button just the quit [x]. className?: string;
fixedWidth: PropTypes.bool, hasCancelButton?: boolean;
className: PropTypes.string, cancelButton?: React.ReactNode;
}; }
static defaultProps = { export default class QuestionDialog extends React.Component<IProps> {
public static defaultProps: Partial<IProps> = {
title: "", title: "",
description: "", description: "",
extraButtons: null, extraButtons: null,
@ -48,17 +49,19 @@ export default class QuestionDialog extends React.Component {
quitOnly: false, quitOnly: false,
}; };
onOk = () => { private onOk = (): void => {
this.props.onFinished(true); this.props.onFinished(true);
}; };
onCancel = () => { private onCancel = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
}; };
render() { public render(): JSX.Element {
// Converting these to imports breaks wrench tests
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let primaryButtonClass = ""; let primaryButtonClass = "";
if (this.props.danger) { if (this.props.danger) {
primaryButtonClass = "danger"; primaryButtonClass = "danger";

View file

@ -17,27 +17,27 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import QuestionDialog from "./QuestionDialog";
import BugReportDialog from "./BugReportDialog";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import { IDialogProps } from "./IDialogProps";
interface IProps extends IDialogProps {
error: string;
}
@replaceableComponent("views.dialogs.SessionRestoreErrorDialog") @replaceableComponent("views.dialogs.SessionRestoreErrorDialog")
export default class SessionRestoreErrorDialog extends React.Component { export default class SessionRestoreErrorDialog extends React.Component<IProps> {
static propTypes = { private sendBugReport = (): void => {
error: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
};
_sendBugReport = () => {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {}); Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {});
}; };
_onClearStorageClick = () => { private onClearStorageClick = (): void => {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Session Restore Confirm Logout', '', QuestionDialog, { Modal.createTrackedDialog('Session Restore Confirm Logout', '', QuestionDialog, {
title: _t("Sign out"), title: _t("Sign out"),
description: description:
@ -48,19 +48,17 @@ export default class SessionRestoreErrorDialog extends React.Component {
}); });
}; };
_onRefreshClick = () => { private onRefreshClick = (): void => {
// Is this likely to help? Probably not, but giving only one button // Is this likely to help? Probably not, but giving only one button
// that clears your storage seems awful. // that clears your storage seems awful.
window.location.reload(true); window.location.reload();
}; };
render() { public render(): JSX.Element {
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const clearStorageButton = ( const clearStorageButton = (
<button onClick={this._onClearStorageClick} className="danger"> <button onClick={this.onClearStorageClick} className="danger">
{ _t("Clear Storage and Sign Out") } { _t("Clear Storage and Sign Out") }
</button> </button>
); );
@ -68,7 +66,7 @@ export default class SessionRestoreErrorDialog extends React.Component {
let dialogButtons; let dialogButtons;
if (SdkConfig.get().bug_report_endpoint_url) { if (SdkConfig.get().bug_report_endpoint_url) {
dialogButtons = <DialogButtons primaryButton={_t("Send Logs")} dialogButtons = <DialogButtons primaryButton={_t("Send Logs")}
onPrimaryButtonClick={this._sendBugReport} onPrimaryButtonClick={this.sendBugReport}
focus={true} focus={true}
hasCancel={false} hasCancel={false}
> >
@ -76,7 +74,7 @@ export default class SessionRestoreErrorDialog extends React.Component {
</DialogButtons>; </DialogButtons>;
} else { } else {
dialogButtons = <DialogButtons primaryButton={_t("Refresh")} dialogButtons = <DialogButtons primaryButton={_t("Refresh")}
onPrimaryButtonClick={this._onRefreshClick} onPrimaryButtonClick={this.onRefreshClick}
focus={true} focus={true}
hasCancel={false} hasCancel={false}
> >

View file

@ -16,13 +16,26 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import * as Email from '../../../email'; import * as Email from '../../../email';
import AddThreepid from '../../../AddThreepid'; import AddThreepid from '../../../AddThreepid';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from "../elements/Spinner";
import ErrorDialog from "./ErrorDialog";
import QuestionDialog from "./QuestionDialog";
import BaseDialog from "./BaseDialog";
import EditableText from "../elements/EditableText";
import { IDialogProps } from "./IDialogProps";
interface IProps extends IDialogProps {
title: string;
}
interface IState {
emailAddress: string;
emailBusy: boolean;
}
/* /*
* Prompt the user to set an email address. * Prompt the user to set an email address.
@ -30,26 +43,25 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
* On success, `onFinished(true)` is called. * On success, `onFinished(true)` is called.
*/ */
@replaceableComponent("views.dialogs.SetEmailDialog") @replaceableComponent("views.dialogs.SetEmailDialog")
export default class SetEmailDialog extends React.Component { export default class SetEmailDialog extends React.Component<IProps, IState> {
static propTypes = { private addThreepid: AddThreepid;
onFinished: PropTypes.func.isRequired,
};
state = { constructor(props: IProps) {
emailAddress: '', super(props);
emailBusy: false,
};
onEmailAddressChanged = value => { this.state = {
emailAddress: '',
emailBusy: false,
};
}
private onEmailAddressChanged = (value: string): void => {
this.setState({ this.setState({
emailAddress: value, emailAddress: value,
}); });
}; };
onSubmit = () => { private onSubmit = (): void => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const emailAddress = this.state.emailAddress; const emailAddress = this.state.emailAddress;
if (!Email.looksValid(emailAddress)) { if (!Email.looksValid(emailAddress)) {
Modal.createTrackedDialog('Invalid Email Address', '', ErrorDialog, { Modal.createTrackedDialog('Invalid Email Address', '', ErrorDialog, {
@ -58,8 +70,8 @@ export default class SetEmailDialog extends React.Component {
}); });
return; return;
} }
this._addThreepid = new AddThreepid(); this.addThreepid = new AddThreepid();
this._addThreepid.addEmailAddress(emailAddress).then(() => { this.addThreepid.addEmailAddress(emailAddress).then(() => {
Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, { Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, {
title: _t("Verification Pending"), title: _t("Verification Pending"),
description: _t( description: _t(
@ -80,11 +92,11 @@ export default class SetEmailDialog extends React.Component {
this.setState({ emailBusy: true }); this.setState({ emailBusy: true });
}; };
onCancelled = () => { private onCancelled = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
}; };
onEmailDialogFinished = ok => { private onEmailDialogFinished = (ok: boolean): void => {
if (ok) { if (ok) {
this.verifyEmailAddress(); this.verifyEmailAddress();
} else { } else {
@ -92,13 +104,12 @@ export default class SetEmailDialog extends React.Component {
} }
}; };
verifyEmailAddress() { private verifyEmailAddress(): void {
this._addThreepid.checkEmailLinkClicked().then(() => { this.addThreepid.checkEmailLinkClicked().then(() => {
this.props.onFinished(true); this.props.onFinished(true);
}, (err) => { }, (err) => {
this.setState({ emailBusy: false }); this.setState({ emailBusy: false });
if (err.errcode == 'M_THREEPID_AUTH_FAILED') { if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const message = _t("Unable to verify email address.") + " " + const message = _t("Unable to verify email address.") + " " +
_t("Please check your email and click on the link it contains. Once this is done, click continue."); _t("Please check your email and click on the link it contains. Once this is done, click continue.");
Modal.createTrackedDialog('Verification Pending', '3pid Auth Failed', QuestionDialog, { Modal.createTrackedDialog('Verification Pending', '3pid Auth Failed', QuestionDialog, {
@ -108,7 +119,6 @@ export default class SetEmailDialog extends React.Component {
onFinished: this.onEmailDialogFinished, onFinished: this.onEmailDialogFinished,
}); });
} else { } else {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to verify email address: " + err); console.error("Unable to verify email address: " + err);
Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, { Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, {
title: _t("Unable to verify email address."), title: _t("Unable to verify email address."),
@ -118,15 +128,10 @@ export default class SetEmailDialog extends React.Component {
}); });
} }
render() { public render(): JSX.Element {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('elements.Spinner');
const EditableText = sdk.getComponent('elements.EditableText');
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
initialValue={this.state.emailAddress} initialValue={this.state.emailAddress}
className="mx_SetEmailDialog_email_input" className="mx_SetEmailDialog_email_input"
autoFocus="true"
placeholder={_t("Email address")} placeholder={_t("Email address")}
placeholderClassName="mx_SetEmailDialog_email_input_placeholder" placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
blurToCancel={false} blurToCancel={false}

View file

@ -17,11 +17,12 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { CommandCategories, Commands } from "../../../SlashCommands"; import { CommandCategories, Commands } from "../../../SlashCommands";
import * as sdk from "../../../index"; import { IDialogProps } from "./IDialogProps";
import InfoDialog from "./InfoDialog";
export default ({ onFinished }) => { interface IProps extends IDialogProps {}
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
const SlashCommandHelpDialog: React.FC<IProps> = ({ onFinished }) => {
const categories = {}; const categories = {};
Commands.forEach(cmd => { Commands.forEach(cmd => {
if (!cmd.isEnabled()) return; if (!cmd.isEnabled()) return;
@ -62,3 +63,5 @@ export default ({ onFinished }) => {
hasCloseButton={true} hasCloseButton={true}
onFinished={onFinished} />; onFinished={onFinished} />;
}; };
export default SlashCommandHelpDialog;

View file

@ -15,40 +15,36 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import BugReportDialog from "./BugReportDialog";
import { IDialogProps } from "./IDialogProps";
interface IProps extends IDialogProps { }
@replaceableComponent("views.dialogs.StorageEvictedDialog") @replaceableComponent("views.dialogs.StorageEvictedDialog")
export default class StorageEvictedDialog extends React.Component { export default class StorageEvictedDialog extends React.Component<IProps> {
static propTypes = { private sendBugReport = (ev: React.MouseEvent): void => {
onFinished: PropTypes.func.isRequired,
};
_sendBugReport = ev => {
ev.preventDefault(); ev.preventDefault();
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
Modal.createTrackedDialog('Storage evicted', 'Send Bug Report Dialog', BugReportDialog, {}); Modal.createTrackedDialog('Storage evicted', 'Send Bug Report Dialog', BugReportDialog, {});
}; };
_onSignOutClick = () => { private onSignOutClick = (): void => {
this.props.onFinished(true); this.props.onFinished(true);
}; };
render() { public render(): JSX.Element {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let logRequest; let logRequest;
if (SdkConfig.get().bug_report_endpoint_url) { if (SdkConfig.get().bug_report_endpoint_url) {
logRequest = _t( logRequest = _t(
"To help us prevent this in future, please <a>send us logs</a>.", "To help us prevent this in future, please <a>send us logs</a>.",
{}, {},
{ {
a: text => <a href="#" onClick={this._sendBugReport}>{ text }</a>, a: text => <a href="#" onClick={this.sendBugReport}>{ text }</a>,
}, },
); );
} }
@ -73,7 +69,7 @@ export default class StorageEvictedDialog extends React.Component {
) } { logRequest }</p> ) } { logRequest }</p>
</div> </div>
<DialogButtons primaryButton={_t("Sign out")} <DialogButtons primaryButton={_t("Sign out")}
onPrimaryButtonClick={this._onSignOutClick} onPrimaryButtonClick={this.onSignOutClick}
focus={true} focus={true}
hasCancel={false} hasCancel={false}
/> />

View file

@ -15,42 +15,47 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import * as sdk from '../../../index';
import { dialogTermsInteractionCallback, TermsNotSignedError } from "../../../Terms"; import { dialogTermsInteractionCallback, TermsNotSignedError } from "../../../Terms";
import classNames from 'classnames'; import classNames from 'classnames';
import * as ScalarMessaging from "../../../ScalarMessaging"; import * as ScalarMessaging from "../../../ScalarMessaging";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IntegrationManagerInstance } from "../../../integrations/IntegrationManagerInstance";
import ScalarAuthClient from "../../../ScalarAuthClient";
import AccessibleButton from "../elements/AccessibleButton";
import IntegrationManager from "../settings/IntegrationManager";
import { IDialogProps } from "./IDialogProps";
interface IProps extends IDialogProps {
/**
* Optional room where the integration manager should be open to
*/
room?: Room;
/**
* Optional screen to open on the integration manager
*/
screen?: string;
/**
* Optional integration ID to open in the integration manager
*/
integrationId?: string;
}
interface IState {
managers: IntegrationManagerInstance[];
busy: boolean;
currentIndex: number;
currentConnected: boolean;
currentLoading: boolean;
currentScalarClient: ScalarAuthClient;
}
@replaceableComponent("views.dialogs.TabbedIntegrationManagerDialog") @replaceableComponent("views.dialogs.TabbedIntegrationManagerDialog")
export default class TabbedIntegrationManagerDialog extends React.Component { export default class TabbedIntegrationManagerDialog extends React.Component<IProps, IState> {
static propTypes = { constructor(props: IProps) {
/**
* Called with:
* * success {bool} True if the user accepted any douments, false if cancelled
* * agreedUrls {string[]} List of agreed URLs
*/
onFinished: PropTypes.func.isRequired,
/**
* Optional room where the integration manager should be open to
*/
room: PropTypes.instanceOf(Room),
/**
* Optional screen to open on the integration manager
*/
screen: PropTypes.string,
/**
* Optional integration ID to open in the integration manager
*/
integrationId: PropTypes.string,
};
constructor(props) {
super(props); super(props);
this.state = { this.state = {
@ -63,11 +68,11 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
}; };
} }
componentDidMount() { public componentDidMount(): void {
this.openManager(0, true); this.openManager(0, true);
} }
openManager = async (i, force = false) => { private openManager = async (i: number, force = false): Promise<void> => {
if (i === this.state.currentIndex && !force) return; if (i === this.state.currentIndex && !force) return;
const manager = this.state.managers[i]; const manager = this.state.managers[i];
@ -120,8 +125,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
} }
}; };
_renderTabs() { private renderTabs(): JSX.Element[] {
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
return this.state.managers.map((m, i) => { return this.state.managers.map((m, i) => {
const classes = classNames({ const classes = classNames({
'mx_TabbedIntegrationManagerDialog_tab': true, 'mx_TabbedIntegrationManagerDialog_tab': true,
@ -140,8 +144,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
}); });
} }
_renderTab() { public renderTab(): JSX.Element {
const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager");
let uiUrl = null; let uiUrl = null;
if (this.state.currentScalarClient) { if (this.state.currentScalarClient) {
uiUrl = this.state.currentScalarClient.getScalarInterfaceUrlForRoom( uiUrl = this.state.currentScalarClient.getScalarInterfaceUrlForRoom(
@ -151,7 +154,6 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
); );
} }
return <IntegrationManager return <IntegrationManager
configured={true}
loading={this.state.currentLoading} loading={this.state.currentLoading}
connected={this.state.currentConnected} connected={this.state.currentConnected}
url={uiUrl} url={uiUrl}
@ -159,14 +161,14 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
/>; />;
} }
render() { public render(): JSX.Element {
return ( return (
<div className='mx_TabbedIntegrationManagerDialog_container'> <div className='mx_TabbedIntegrationManagerDialog_container'>
<div className='mx_TabbedIntegrationManagerDialog_tabs'> <div className='mx_TabbedIntegrationManagerDialog_tabs'>
{ this._renderTabs() } { this.renderTabs() }
</div> </div>
<div className='mx_TabbedIntegrationManagerDialog_currentManager'> <div className='mx_TabbedIntegrationManagerDialog_currentManager'>
{ this._renderTab() } { this.renderTab() }
</div> </div>
</div> </div>
); );

View file

@ -14,33 +14,39 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { createRef } from 'react'; import React, { ChangeEvent, createRef } from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import Field from "../elements/Field"; import Field from "../elements/Field";
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IFieldState, IValidationResult } from "../elements/Validation";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import { IDialogProps } from "./IDialogProps";
interface IProps extends IDialogProps {
title?: string;
description?: React.ReactNode;
value?: string;
placeholder?: string;
button?: string;
busyMessage?: string; // pass _td string
focus?: boolean;
hasCancel?: boolean;
validator?: (fieldState: IFieldState) => IValidationResult; // result of withValidation
fixedWidth?: boolean;
}
interface IState {
value: string;
busy: boolean;
valid: boolean;
}
@replaceableComponent("views.dialogs.TextInputDialog") @replaceableComponent("views.dialogs.TextInputDialog")
export default class TextInputDialog extends React.Component { export default class TextInputDialog extends React.Component<IProps, IState> {
static propTypes = { private field = createRef<Field>();
title: PropTypes.string,
description: PropTypes.oneOfType([
PropTypes.element,
PropTypes.string,
]),
value: PropTypes.string,
placeholder: PropTypes.string,
button: PropTypes.string,
busyMessage: PropTypes.string, // pass _td string
focus: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
hasCancel: PropTypes.bool,
validator: PropTypes.func, // result of withValidation
fixedWidth: PropTypes.bool,
};
static defaultProps = { public static defaultProps = {
title: "", title: "",
value: "", value: "",
description: "", description: "",
@ -49,11 +55,9 @@ export default class TextInputDialog extends React.Component {
hasCancel: true, hasCancel: true,
}; };
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this._field = createRef();
this.state = { this.state = {
value: this.props.value, value: this.props.value,
busy: false, busy: false,
@ -61,23 +65,23 @@ export default class TextInputDialog extends React.Component {
}; };
} }
componentDidMount() { public componentDidMount(): void {
if (this.props.focus) { if (this.props.focus) {
// Set the cursor at the end of the text input // Set the cursor at the end of the text input
// this._field.current.value = this.props.value; // this._field.current.value = this.props.value;
this._field.current.focus(); this.field.current.focus();
} }
} }
onOk = async ev => { private onOk = async (ev: React.FormEvent): Promise<void> => {
ev.preventDefault(); ev.preventDefault();
if (this.props.validator) { if (this.props.validator) {
this.setState({ busy: true }); this.setState({ busy: true });
await this._field.current.validate({ allowEmpty: false }); await this.field.current.validate({ allowEmpty: false });
if (!this._field.current.state.valid) { if (!this.field.current.state.valid) {
this._field.current.focus(); this.field.current.focus();
this._field.current.validate({ allowEmpty: false, focused: true }); this.field.current.validate({ allowEmpty: false, focused: true });
this.setState({ busy: false }); this.setState({ busy: false });
return; return;
} }
@ -85,17 +89,17 @@ export default class TextInputDialog extends React.Component {
this.props.onFinished(true, this.state.value); this.props.onFinished(true, this.state.value);
}; };
onCancel = () => { private onCancel = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
}; };
onChange = ev => { private onChange = (ev: ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
value: ev.target.value, value: ev.target.value,
}); });
}; };
onValidate = async fieldState => { private onValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
const result = await this.props.validator(fieldState); const result = await this.props.validator(fieldState);
this.setState({ this.setState({
valid: result.valid, valid: result.valid,
@ -103,9 +107,7 @@ export default class TextInputDialog extends React.Component {
return result; return result;
}; };
render() { public render(): JSX.Element {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return ( return (
<BaseDialog <BaseDialog
className="mx_TextInputDialog" className="mx_TextInputDialog"
@ -121,13 +123,12 @@ export default class TextInputDialog extends React.Component {
<div> <div>
<Field <Field
className="mx_TextInputDialog_input" className="mx_TextInputDialog_input"
ref={this._field} ref={this.field}
type="text" type="text"
label={this.props.placeholder} label={this.props.placeholder}
value={this.state.value} value={this.state.value}
onChange={this.onChange} onChange={this.onChange}
onValidate={this.props.validator ? this.onValidate : undefined} onValidate={this.props.validator ? this.onValidate : undefined}
size="64"
/> />
</div> </div>
</div> </div>

View file

@ -19,7 +19,7 @@ import { User } from "matrix-js-sdk/src/models/user";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import E2EIcon from "../rooms/E2EIcon"; import E2EIcon, { E2EState } from "../rooms/E2EIcon";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import { IDialogProps } from "./IDialogProps"; import { IDialogProps } from "./IDialogProps";
@ -47,7 +47,7 @@ const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) =
onFinished={onFinished} onFinished={onFinished}
className="mx_UntrustedDeviceDialog" className="mx_UntrustedDeviceDialog"
title={<> title={<>
<E2EIcon status="warning" size={24} hideTooltip={true} /> <E2EIcon status={E2EState.Warning} size={24} hideTooltip={true} />
{ _t("Not Trusted") } { _t("Not Trusted") }
</>} </>}
> >

View file

@ -17,11 +17,18 @@ limitations under the License.
import filesize from 'filesize'; import filesize from 'filesize';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import ContentMessages from '../../../ContentMessages'; import ContentMessages from '../../../ContentMessages';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import { IDialogProps } from "./IDialogProps";
interface IProps extends IDialogProps {
badFiles: File[];
totalFiles: number;
contentMessages: ContentMessages;
}
/* /*
* Tells the user about files we know cannot be uploaded before we even try uploading * Tells the user about files we know cannot be uploaded before we even try uploading
@ -29,26 +36,16 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
* the size of the file. * the size of the file.
*/ */
@replaceableComponent("views.dialogs.UploadFailureDialog") @replaceableComponent("views.dialogs.UploadFailureDialog")
export default class UploadFailureDialog extends React.Component { export default class UploadFailureDialog extends React.Component<IProps> {
static propTypes = { private onCancelClick = (): void => {
badFiles: PropTypes.arrayOf(PropTypes.object).isRequired,
totalFiles: PropTypes.number.isRequired,
contentMessages: PropTypes.instanceOf(ContentMessages).isRequired,
onFinished: PropTypes.func.isRequired,
}
_onCancelClick = () => {
this.props.onFinished(false); this.props.onFinished(false);
} };
_onUploadClick = () => { private onUploadClick = (): void => {
this.props.onFinished(true); this.props.onFinished(true);
} };
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
public render(): JSX.Element {
let message; let message;
let preview; let preview;
let buttons; let buttons;
@ -65,7 +62,7 @@ export default class UploadFailureDialog extends React.Component {
); );
buttons = <DialogButtons primaryButton={_t('OK')} buttons = <DialogButtons primaryButton={_t('OK')}
hasCancel={false} hasCancel={false}
onPrimaryButtonClick={this._onCancelClick} onPrimaryButtonClick={this.onCancelClick}
focus={true} focus={true}
/>; />;
} else if (this.props.totalFiles === this.props.badFiles.length) { } else if (this.props.totalFiles === this.props.badFiles.length) {
@ -80,7 +77,7 @@ export default class UploadFailureDialog extends React.Component {
); );
buttons = <DialogButtons primaryButton={_t('OK')} buttons = <DialogButtons primaryButton={_t('OK')}
hasCancel={false} hasCancel={false}
onPrimaryButtonClick={this._onCancelClick} onPrimaryButtonClick={this.onCancelClick}
focus={true} focus={true}
/>; />;
} else { } else {
@ -96,17 +93,17 @@ export default class UploadFailureDialog extends React.Component {
const howManyOthers = this.props.totalFiles - this.props.badFiles.length; const howManyOthers = this.props.totalFiles - this.props.badFiles.length;
buttons = <DialogButtons buttons = <DialogButtons
primaryButton={_t('Upload %(count)s other files', { count: howManyOthers })} primaryButton={_t('Upload %(count)s other files', { count: howManyOthers })}
onPrimaryButtonClick={this._onUploadClick} onPrimaryButtonClick={this.onUploadClick}
hasCancel={true} hasCancel={true}
cancelButton={_t("Cancel All")} cancelButton={_t("Cancel All")}
onCancel={this._onCancelClick} onCancel={this.onCancelClick}
focus={true} focus={true}
/>; />;
} }
return ( return (
<BaseDialog className='mx_UploadFailureDialog' <BaseDialog className='mx_UploadFailureDialog'
onFinished={this._onCancelClick} onFinished={this.onCancelClick}
title={_t("Upload Error")} title={_t("Upload Error")}
contentId='mx_Dialog_content' contentId='mx_Dialog_content'
> >

View file

@ -33,6 +33,7 @@ import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab
import { UIFeature } from "../../../settings/UIFeature"; import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import { IDialogProps } from "./IDialogProps";
export enum UserTab { export enum UserTab {
General = "USER_GENERAL_TAB", General = "USER_GENERAL_TAB",
@ -47,8 +48,7 @@ export enum UserTab {
Help = "USER_HELP_TAB", Help = "USER_HELP_TAB",
} }
interface IProps { interface IProps extends IDialogProps {
onFinished: (success: boolean) => void;
initialTabId?: string; initialTabId?: string;
} }

View file

@ -25,6 +25,8 @@ import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons"; import DialogButtons from "../elements/DialogButtons";
import { logger } from "matrix-js-sdk/src/logger";
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
widget: Widget; widget: Widget;
widgetKind: WidgetKind; widgetKind: WidgetKind;
@ -45,17 +47,17 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent<I
}; };
} }
private onAllow = () => { private onAllow = (): void => {
this.onPermissionSelection(true); this.onPermissionSelection(true);
}; };
private onDeny = () => { private onDeny = (): void => {
this.onPermissionSelection(false); this.onPermissionSelection(false);
}; };
private onPermissionSelection(allowed: boolean) { private onPermissionSelection(allowed: boolean): void {
if (this.state.rememberSelection) { if (this.state.rememberSelection) {
console.log(`Remembering ${this.props.widget.id} as allowed=${allowed} for OpenID`); logger.log(`Remembering ${this.props.widget.id} as allowed=${allowed} for OpenID`);
WidgetPermissionStore.instance.setOIDCState( WidgetPermissionStore.instance.setOIDCState(
this.props.widget, this.props.widgetKind, this.props.inRoomId, this.props.widget, this.props.widgetKind, this.props.inRoomId,
@ -66,11 +68,11 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent<I
this.props.onFinished(allowed); this.props.onFinished(allowed);
} }
private onRememberSelectionChange = (newVal: boolean) => { private onRememberSelectionChange = (newVal: boolean): void => {
this.setState({ rememberSelection: newVal }); this.setState({ rememberSelection: newVal });
}; };
public render() { public render(): JSX.Element {
return ( return (
<BaseDialog <BaseDialog
className='mx_WidgetOpenIDPermissionsDialog' className='mx_WidgetOpenIDPermissionsDialog'

View file

@ -28,6 +28,8 @@ import Spinner from '../../elements/Spinner';
import InteractiveAuthDialog from '../InteractiveAuthDialog'; import InteractiveAuthDialog from '../InteractiveAuthDialog';
import { replaceableComponent } from "../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../utils/replaceableComponent";
import { logger } from "matrix-js-sdk/src/logger";
interface IProps { interface IProps {
accountPassword?: string; accountPassword?: string;
tokenLogin?: boolean; tokenLogin?: boolean;
@ -77,10 +79,10 @@ export default class CreateCrossSigningDialog extends React.PureComponent<IProps
// We should never get here: the server should always require // We should never get here: the server should always require
// UI auth to upload device signing keys. If we do, we upload // UI auth to upload device signing keys. If we do, we upload
// no keys which would be a no-op. // no keys which would be a no-op.
console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!"); logger.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
} catch (error) { } catch (error) {
if (!error.data || !error.data.flows) { if (!error.data || !error.data.flows) {
console.log("uploadDeviceSigningKeys advertised no flows!"); logger.log("uploadDeviceSigningKeys advertised no flows!");
return; return;
} }
const canUploadKeysWithPasswordOnly = error.data.flows.some(f => { const canUploadKeysWithPasswordOnly = error.data.flows.some(f => {

View file

@ -16,30 +16,64 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../../index';
import { MatrixClientPeg } from '../../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import { MatrixClient } from 'matrix-js-sdk/src/client'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../SecurityManager'; import { accessSecretStorage } from '../../../../SecurityManager';
import { IKeyBackupInfo, IKeyBackupRestoreResult } from "matrix-js-sdk/src/crypto/keybackup";
import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api";
import * as sdk from '../../../../index';
import { IDialogProps } from "../IDialogProps";
import { logger } from "matrix-js-sdk/src/logger";
const RESTORE_TYPE_PASSPHRASE = 0; enum RestoreType {
const RESTORE_TYPE_RECOVERYKEY = 1; Passphrase = "passphrase",
const RESTORE_TYPE_SECRET_STORAGE = 2; RecoveryKey = "recovery_key",
SecretStorage = "secret_storage"
}
enum ProgressState {
PreFetch = "prefetch",
Fetch = "fetch",
LoadKeys = "load_keys",
}
interface IProps extends IDialogProps {
// if false, will close the dialog as soon as the restore completes succesfully
// default: true
showSummary?: boolean;
// If specified, gather the key from the user but then call the function with the backup
// key rather than actually (necessarily) restoring the backup.
keyCallback?: (key: Uint8Array) => void;
}
interface IState {
backupInfo: IKeyBackupInfo;
backupKeyStored: Record<string, ISecretStorageKeyInfo>;
loading: boolean;
loadError: string;
restoreError: {
errcode: string;
};
recoveryKey: string;
recoverInfo: IKeyBackupRestoreResult;
recoveryKeyValid: boolean;
forceRecoveryKey: boolean;
passPhrase: string;
restoreType: RestoreType;
progress: {
stage: ProgressState;
total?: number;
successes?: number;
failures?: number;
};
}
/* /*
* Dialog for restoring e2e keys from a backup and the user's recovery key * Dialog for restoring e2e keys from a backup and the user's recovery key
*/ */
export default class RestoreKeyBackupDialog extends React.PureComponent { export default class RestoreKeyBackupDialog extends React.PureComponent<IProps, IState> {
static propTypes = {
// if false, will close the dialog as soon as the restore completes succesfully
// default: true
showSummary: PropTypes.bool,
// If specified, gather the key from the user but then call the function with the backup
// key rather than actually (necessarily) restoring the backup.
keyCallback: PropTypes.func,
};
static defaultProps = { static defaultProps = {
showSummary: true, showSummary: true,
}; };
@ -58,58 +92,58 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
forceRecoveryKey: false, forceRecoveryKey: false,
passPhrase: '', passPhrase: '',
restoreType: null, restoreType: null,
progress: { stage: "prefetch" }, progress: { stage: ProgressState.PreFetch },
}; };
} }
componentDidMount() { public componentDidMount(): void {
this._loadBackupStatus(); this.loadBackupStatus();
} }
_onCancel = () => { private onCancel = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
} };
_onDone = () => { private onDone = (): void => {
this.props.onFinished(true); this.props.onFinished(true);
} };
_onUseRecoveryKeyClick = () => { private onUseRecoveryKeyClick = (): void => {
this.setState({ this.setState({
forceRecoveryKey: true, forceRecoveryKey: true,
}); });
} };
_progressCallback = (data) => { private progressCallback = (data): void => {
this.setState({ this.setState({
progress: data, progress: data,
}); });
} };
_onResetRecoveryClick = () => { private onResetRecoveryClick = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
accessSecretStorage(() => {}, /* forceReset = */ true); accessSecretStorage(async () => {}, /* forceReset = */ true);
} };
_onRecoveryKeyChange = (e) => { private onRecoveryKeyChange = (e): void => {
this.setState({ this.setState({
recoveryKey: e.target.value, recoveryKey: e.target.value,
recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value), recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
}); });
} };
_onPassPhraseNext = async () => { private onPassPhraseNext = async (): Promise<void> => {
this.setState({ this.setState({
loading: true, loading: true,
restoreError: null, restoreError: null,
restoreType: RESTORE_TYPE_PASSPHRASE, restoreType: RestoreType.Passphrase,
}); });
try { try {
// We do still restore the key backup: we must ensure that the key backup key // We do still restore the key backup: we must ensure that the key backup key
// is the right one and restoring it is currently the only way we can do this. // is the right one and restoring it is currently the only way we can do this.
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword( const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword(
this.state.passPhrase, undefined, undefined, this.state.backupInfo, this.state.passPhrase, undefined, undefined, this.state.backupInfo,
{ progressCallback: this._progressCallback }, { progressCallback: this.progressCallback },
); );
if (this.props.keyCallback) { if (this.props.keyCallback) {
const key = await MatrixClientPeg.get().keyBackupKeyFromPassword( const key = await MatrixClientPeg.get().keyBackupKeyFromPassword(
@ -127,26 +161,26 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
recoverInfo, recoverInfo,
}); });
} catch (e) { } catch (e) {
console.log("Error restoring backup", e); logger.log("Error restoring backup", e);
this.setState({ this.setState({
loading: false, loading: false,
restoreError: e, restoreError: e,
}); });
} }
} };
_onRecoveryKeyNext = async () => { private onRecoveryKeyNext = async (): Promise<void> => {
if (!this.state.recoveryKeyValid) return; if (!this.state.recoveryKeyValid) return;
this.setState({ this.setState({
loading: true, loading: true,
restoreError: null, restoreError: null,
restoreType: RESTORE_TYPE_RECOVERYKEY, restoreType: RestoreType.RecoveryKey,
}); });
try { try {
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey( const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey(
this.state.recoveryKey, undefined, undefined, this.state.backupInfo, this.state.recoveryKey, undefined, undefined, this.state.backupInfo,
{ progressCallback: this._progressCallback }, { progressCallback: this.progressCallback },
); );
if (this.props.keyCallback) { if (this.props.keyCallback) {
const key = MatrixClientPeg.get().keyBackupKeyFromRecoveryKey(this.state.recoveryKey); const key = MatrixClientPeg.get().keyBackupKeyFromRecoveryKey(this.state.recoveryKey);
@ -161,40 +195,39 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
recoverInfo, recoverInfo,
}); });
} catch (e) { } catch (e) {
console.log("Error restoring backup", e); logger.log("Error restoring backup", e);
this.setState({ this.setState({
loading: false, loading: false,
restoreError: e, restoreError: e,
}); });
} }
} };
_onPassPhraseChange = (e) => { private onPassPhraseChange = (e): void => {
this.setState({ this.setState({
passPhrase: e.target.value, passPhrase: e.target.value,
}); });
} };
async _restoreWithSecretStorage() { private async restoreWithSecretStorage(): Promise<void> {
this.setState({ this.setState({
loading: true, loading: true,
restoreError: null, restoreError: null,
restoreType: RESTORE_TYPE_SECRET_STORAGE, restoreType: RestoreType.SecretStorage,
}); });
try { try {
// `accessSecretStorage` may prompt for storage access as needed. // `accessSecretStorage` may prompt for storage access as needed.
const recoverInfo = await accessSecretStorage(async () => { await accessSecretStorage(async () => {
return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage( await MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
this.state.backupInfo, undefined, undefined, this.state.backupInfo, undefined, undefined,
{ progressCallback: this._progressCallback }, { progressCallback: this.progressCallback },
); );
}); });
this.setState({ this.setState({
loading: false, loading: false,
recoverInfo,
}); });
} catch (e) { } catch (e) {
console.log("Error restoring backup", e); logger.log("Error restoring backup", e);
this.setState({ this.setState({
restoreError: e, restoreError: e,
loading: false, loading: false,
@ -202,26 +235,26 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
} }
} }
async _restoreWithCachedKey(backupInfo) { private async restoreWithCachedKey(backupInfo): Promise<boolean> {
if (!backupInfo) return false; if (!backupInfo) return false;
try { try {
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithCache( const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithCache(
undefined, /* targetRoomId */ undefined, /* targetRoomId */
undefined, /* targetSessionId */ undefined, /* targetSessionId */
backupInfo, backupInfo,
{ progressCallback: this._progressCallback }, { progressCallback: this.progressCallback },
); );
this.setState({ this.setState({
recoverInfo, recoverInfo,
}); });
return true; return true;
} catch (e) { } catch (e) {
console.log("restoreWithCachedKey failed:", e); logger.log("restoreWithCachedKey failed:", e);
return false; return false;
} }
} }
async _loadBackupStatus() { private async loadBackupStatus(): Promise<void> {
this.setState({ this.setState({
loading: true, loading: true,
loadError: null, loadError: null,
@ -230,15 +263,15 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const backupInfo = await cli.getKeyBackupVersion(); const backupInfo = await cli.getKeyBackupVersion();
const has4S = await cli.hasSecretStorageKey(); const has4S = await cli.hasSecretStorageKey();
const backupKeyStored = has4S && await cli.isKeyBackupKeyStored(); const backupKeyStored = has4S && (await cli.isKeyBackupKeyStored());
this.setState({ this.setState({
backupInfo, backupInfo,
backupKeyStored, backupKeyStored,
}); });
const gotCache = await this._restoreWithCachedKey(backupInfo); const gotCache = await this.restoreWithCachedKey(backupInfo);
if (gotCache) { if (gotCache) {
console.log("RestoreKeyBackupDialog: found cached backup key"); logger.log("RestoreKeyBackupDialog: found cached backup key");
this.setState({ this.setState({
loading: false, loading: false,
}); });
@ -247,7 +280,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
// If the backup key is stored, we can proceed directly to restore. // If the backup key is stored, we can proceed directly to restore.
if (backupKeyStored) { if (backupKeyStored) {
return this._restoreWithSecretStorage(); return this.restoreWithSecretStorage();
} }
this.setState({ this.setState({
@ -255,7 +288,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
loading: false, loading: false,
}); });
} catch (e) { } catch (e) {
console.log("Error loading backup status", e); logger.log("Error loading backup status", e);
this.setState({ this.setState({
loadError: e, loadError: e,
loading: false, loading: false,
@ -263,7 +296,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
} }
} }
render() { public render(): JSX.Element {
// FIXME: Making these into imports will break tests
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
@ -279,12 +315,12 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
if (this.state.loading) { if (this.state.loading) {
title = _t("Restoring keys from backup"); title = _t("Restoring keys from backup");
let details; let details;
if (this.state.progress.stage === "fetch") { if (this.state.progress.stage === ProgressState.Fetch) {
details = _t("Fetching keys from server..."); details = _t("Fetching keys from server...");
} else if (this.state.progress.stage === "load_keys") { } else if (this.state.progress.stage === ProgressState.LoadKeys) {
const { total, successes, failures } = this.state.progress; const { total, successes, failures } = this.state.progress;
details = _t("%(completed)s of %(total)s keys restored", { total, completed: successes + failures }); details = _t("%(completed)s of %(total)s keys restored", { total, completed: successes + failures });
} else if (this.state.progress.stage === "prefetch") { } else if (this.state.progress.stage === ProgressState.PreFetch) {
details = _t("Fetching keys from server..."); details = _t("Fetching keys from server...");
} }
content = <div> content = <div>
@ -296,7 +332,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
content = _t("Unable to load backup status"); content = _t("Unable to load backup status");
} else if (this.state.restoreError) { } else if (this.state.restoreError) {
if (this.state.restoreError.errcode === MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY) { if (this.state.restoreError.errcode === MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY) {
if (this.state.restoreType === RESTORE_TYPE_RECOVERYKEY) { if (this.state.restoreType === RestoreType.RecoveryKey) {
title = _t("Security Key mismatch"); title = _t("Security Key mismatch");
content = <div> content = <div>
<p>{ _t( <p>{ _t(
@ -321,7 +357,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
title = _t("Error"); title = _t("Error");
content = _t("No backup found!"); content = _t("No backup found!");
} else if (this.state.recoverInfo) { } else if (this.state.recoverInfo) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
title = _t("Keys restored"); title = _t("Keys restored");
let failedToDecrypt; let failedToDecrypt;
if (this.state.recoverInfo.total > this.state.recoverInfo.imported) { if (this.state.recoverInfo.total > this.state.recoverInfo.imported) {
@ -334,14 +369,12 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
<p>{ _t("Successfully restored %(sessionCount)s keys", { sessionCount: this.state.recoverInfo.imported }) }</p> <p>{ _t("Successfully restored %(sessionCount)s keys", { sessionCount: this.state.recoverInfo.imported }) }</p>
{ failedToDecrypt } { failedToDecrypt }
<DialogButtons primaryButton={_t('OK')} <DialogButtons primaryButton={_t('OK')}
onPrimaryButtonClick={this._onDone} onPrimaryButtonClick={this.onDone}
hasCancel={false} hasCancel={false}
focus={true} focus={true}
/> />
</div>; </div>;
} else if (backupHasPassphrase && !this.state.forceRecoveryKey) { } else if (backupHasPassphrase && !this.state.forceRecoveryKey) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Enter Security Phrase"); title = _t("Enter Security Phrase");
content = <div> content = <div>
<p>{ _t( <p>{ _t(
@ -357,16 +390,16 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
<form className="mx_RestoreKeyBackupDialog_primaryContainer"> <form className="mx_RestoreKeyBackupDialog_primaryContainer">
<input type="password" <input type="password"
className="mx_RestoreKeyBackupDialog_passPhraseInput" className="mx_RestoreKeyBackupDialog_passPhraseInput"
onChange={this._onPassPhraseChange} onChange={this.onPassPhraseChange}
value={this.state.passPhrase} value={this.state.passPhrase}
autoFocus={true} autoFocus={true}
/> />
<DialogButtons <DialogButtons
primaryButton={_t('Next')} primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseNext} onPrimaryButtonClick={this.onPassPhraseNext}
primaryIsSubmit={true} primaryIsSubmit={true}
hasCancel={true} hasCancel={true}
onCancel={this._onCancel} onCancel={this.onCancel}
focus={false} focus={false}
/> />
</form> </form>
@ -379,14 +412,14 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
button1: s => <AccessibleButton button1: s => <AccessibleButton
className="mx_linkButton" className="mx_linkButton"
element="span" element="span"
onClick={this._onUseRecoveryKeyClick} onClick={this.onUseRecoveryKeyClick}
> >
{ s } { s }
</AccessibleButton>, </AccessibleButton>,
button2: s => <AccessibleButton button2: s => <AccessibleButton
className="mx_linkButton" className="mx_linkButton"
element="span" element="span"
onClick={this._onResetRecoveryClick} onClick={this.onResetRecoveryClick}
> >
{ s } { s }
</AccessibleButton>, </AccessibleButton>,
@ -394,8 +427,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
</div>; </div>;
} else { } else {
title = _t("Enter Security Key"); title = _t("Enter Security Key");
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let keyStatus; let keyStatus;
if (this.state.recoveryKey.length === 0) { if (this.state.recoveryKey.length === 0) {
@ -423,15 +454,15 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
<div className="mx_RestoreKeyBackupDialog_primaryContainer"> <div className="mx_RestoreKeyBackupDialog_primaryContainer">
<input className="mx_RestoreKeyBackupDialog_recoveryKeyInput" <input className="mx_RestoreKeyBackupDialog_recoveryKeyInput"
onChange={this._onRecoveryKeyChange} onChange={this.onRecoveryKeyChange}
value={this.state.recoveryKey} value={this.state.recoveryKey}
autoFocus={true} autoFocus={true}
/> />
{ keyStatus } { keyStatus }
<DialogButtons primaryButton={_t('Next')} <DialogButtons primaryButton={_t('Next')}
onPrimaryButtonClick={this._onRecoveryKeyNext} onPrimaryButtonClick={this.onRecoveryKeyNext}
hasCancel={true} hasCancel={true}
onCancel={this._onCancel} onCancel={this.onCancel}
focus={false} focus={false}
primaryDisabled={!this.state.recoveryKeyValid} primaryDisabled={!this.state.recoveryKeyValid}
/> />
@ -443,7 +474,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
{ {
button: s => <AccessibleButton className="mx_linkButton" button: s => <AccessibleButton className="mx_linkButton"
element="span" element="span"
onClick={this._onResetRecoveryClick} onClick={this.onResetRecoveryClick}
> >
{ s } { s }
</AccessibleButton>, </AccessibleButton>,

View file

@ -20,6 +20,7 @@ import BaseDialog from '../BaseDialog';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import { SetupEncryptionStore, Phase } from '../../../../stores/SetupEncryptionStore'; import { SetupEncryptionStore, Phase } from '../../../../stores/SetupEncryptionStore';
import { replaceableComponent } from "../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../utils/replaceableComponent";
import { IDialogProps } from "../IDialogProps";
function iconFromPhase(phase: Phase) { function iconFromPhase(phase: Phase) {
if (phase === Phase.Done) { if (phase === Phase.Done) {
@ -29,12 +30,9 @@ function iconFromPhase(phase: Phase) {
} }
} }
interface IProps { interface IProps extends IDialogProps {}
onFinished: (success: boolean) => void;
}
interface IState { interface IState {
icon: Phase; icon: string;
} }
@replaceableComponent("views.dialogs.security.SetupEncryptionDialog") @replaceableComponent("views.dialogs.security.SetupEncryptionDialog")

View file

@ -19,7 +19,6 @@ limitations under the License.
import url from 'url'; import url from 'url';
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import AccessibleButton from './AccessibleButton'; import AccessibleButton from './AccessibleButton';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -39,33 +38,97 @@ import { MatrixCapabilities } from "matrix-widget-api";
import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu"; import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu";
import WidgetAvatar from "../avatars/WidgetAvatar"; import WidgetAvatar from "../avatars/WidgetAvatar";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { Room } from "matrix-js-sdk/src/models/room";
import { IApp } from "../../../stores/WidgetStore";
interface IProps {
app: IApp;
// If room is not specified then it is an account level widget
// which bypasses permission prompts as it was added explicitly by that user
room: Room;
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
fullWidth?: boolean;
// Optional. If set, renders a smaller view of the widget
miniMode?: boolean;
// UserId of the current user
userId: string;
// UserId of the entity that added / modified the widget
creatorUserId: string;
waitForIframeLoad: boolean;
showMenubar?: boolean;
// Optional onEditClickHandler (overrides default behaviour)
onEditClick?: () => void;
// Optional onDeleteClickHandler (overrides default behaviour)
onDeleteClick?: () => void;
// Optionally hide the tile title
showTitle?: boolean;
// Optionally handle minimise button pointer events (default false)
handleMinimisePointerEvents?: boolean;
// Optionally hide the popout widget icon
showPopout?: boolean;
// Is this an instance of a user widget
userWidget: boolean;
// sets the pointer-events property on the iframe
pointerEvents?: string;
widgetPageTitle?: string;
}
interface IState {
initialising: boolean; // True while we are mangling the widget URL
// True while the iframe content is loading
loading: boolean;
// Assume that widget has permission to load if we are the user who
// added it to the room, or if explicitly granted by the user
hasPermissionToLoad: boolean;
error: Error;
menuDisplayed: boolean;
widgetPageTitle: string;
}
import { logger } from "matrix-js-sdk/src/logger";
@replaceableComponent("views.elements.AppTile") @replaceableComponent("views.elements.AppTile")
export default class AppTile extends React.Component { export default class AppTile extends React.Component<IProps, IState> {
constructor(props) { public static defaultProps: Partial<IProps> = {
waitForIframeLoad: true,
showMenubar: true,
showTitle: true,
showPopout: true,
handleMinimisePointerEvents: false,
userWidget: false,
miniMode: false,
};
private contextMenuButton = createRef<any>();
private iframe: HTMLIFrameElement; // ref to the iframe (callback style)
private allowedWidgetsWatchRef: string;
private persistKey: string;
private sgWidget: StopGapWidget;
private dispatcherRef: string;
constructor(props: IProps) {
super(props); super(props);
// The key used for PersistedElement // The key used for PersistedElement
this._persistKey = getPersistKey(this.props.app.id); this.persistKey = getPersistKey(this.props.app.id);
try { try {
this._sgWidget = new StopGapWidget(this.props); this.sgWidget = new StopGapWidget(this.props);
this._sgWidget.on("preparing", this._onWidgetPrepared); this.sgWidget.on("preparing", this.onWidgetPrepared);
this._sgWidget.on("ready", this._onWidgetReady); this.sgWidget.on("ready", this.onWidgetReady);
} catch (e) { } catch (e) {
console.log("Failed to construct widget", e); logger.log("Failed to construct widget", e);
this._sgWidget = null; this.sgWidget = null;
} }
this.iframe = null; // ref to the iframe (callback style)
this.state = this._getNewState(props); this.state = this.getNewState(props);
this._contextMenuButton = createRef();
this._allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange); this.allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange);
} }
// This is a function to make the impact of calling SettingsStore slightly less // This is a function to make the impact of calling SettingsStore slightly less
hasPermissionToLoad = (props) => { private hasPermissionToLoad = (props: IProps): boolean => {
if (this._usingLocalWidget()) return true; if (this.usingLocalWidget()) return true;
if (!props.room) return true; // user widgets always have permissions if (!props.room) return true; // user widgets always have permissions
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId); const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId);
@ -81,34 +144,34 @@ export default class AppTile extends React.Component {
* @param {Object} newProps The new properties of the component * @param {Object} newProps The new properties of the component
* @return {Object} Updated component state to be set with setState * @return {Object} Updated component state to be set with setState
*/ */
_getNewState(newProps) { private getNewState(newProps: IProps): IState {
return { return {
initialising: true, // True while we are mangling the widget URL initialising: true, // True while we are mangling the widget URL
// True while the iframe content is loading // True while the iframe content is loading
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey), loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this.persistKey),
// Assume that widget has permission to load if we are the user who // Assume that widget has permission to load if we are the user who
// added it to the room, or if explicitly granted by the user // added it to the room, or if explicitly granted by the user
hasPermissionToLoad: this.hasPermissionToLoad(newProps), hasPermissionToLoad: this.hasPermissionToLoad(newProps),
error: null, error: null,
widgetPageTitle: newProps.widgetPageTitle,
menuDisplayed: false, menuDisplayed: false,
widgetPageTitle: this.props.widgetPageTitle,
}; };
} }
onAllowedWidgetsChange = () => { private onAllowedWidgetsChange = (): void => {
const hasPermissionToLoad = this.hasPermissionToLoad(this.props); const hasPermissionToLoad = this.hasPermissionToLoad(this.props);
if (this.state.hasPermissionToLoad && !hasPermissionToLoad) { if (this.state.hasPermissionToLoad && !hasPermissionToLoad) {
// Force the widget to be non-persistent (able to be deleted/forgotten) // Force the widget to be non-persistent (able to be deleted/forgotten)
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id);
PersistedElement.destroyElement(this._persistKey); PersistedElement.destroyElement(this.persistKey);
if (this._sgWidget) this._sgWidget.stop(); if (this.sgWidget) this.sgWidget.stop();
} }
this.setState({ hasPermissionToLoad }); this.setState({ hasPermissionToLoad });
}; };
isMixedContent() { private isMixedContent(): boolean {
const parentContentProtocol = window.location.protocol; const parentContentProtocol = window.location.protocol;
const u = url.parse(this.props.app.url); const u = url.parse(this.props.app.url);
const childContentProtocol = u.protocol; const childContentProtocol = u.protocol;
@ -120,69 +183,70 @@ export default class AppTile extends React.Component {
return false; return false;
} }
componentDidMount() { public componentDidMount(): void {
// Only fetch IM token on mount if we're showing and have permission to load // Only fetch IM token on mount if we're showing and have permission to load
if (this._sgWidget && this.state.hasPermissionToLoad) { if (this.sgWidget && this.state.hasPermissionToLoad) {
this._startWidget(); this.startWidget();
} }
// Widget action listeners // Widget action listeners
this.dispatcherRef = dis.register(this._onAction); this.dispatcherRef = dis.register(this.onAction);
} }
componentWillUnmount() { public componentWillUnmount(): void {
// Widget action listeners // Widget action listeners
if (this.dispatcherRef) dis.unregister(this.dispatcherRef); if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
// if it's not remaining on screen, get rid of the PersistedElement container // if it's not remaining on screen, get rid of the PersistedElement container
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) { if (!ActiveWidgetStore.instance.getWidgetPersistence(this.props.app.id)) {
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id);
PersistedElement.destroyElement(this._persistKey); PersistedElement.destroyElement(this.persistKey);
} }
if (this._sgWidget) { if (this.sgWidget) {
this._sgWidget.stop(); this.sgWidget.stop();
} }
SettingsStore.unwatchSetting(this._allowedWidgetsWatchRef); SettingsStore.unwatchSetting(this.allowedWidgetsWatchRef);
} }
_resetWidget(newProps) { private resetWidget(newProps: IProps): void {
if (this._sgWidget) { if (this.sgWidget) {
this._sgWidget.stop(); this.sgWidget.stop();
} }
try { try {
this._sgWidget = new StopGapWidget(newProps); this.sgWidget = new StopGapWidget(newProps);
this._sgWidget.on("preparing", this._onWidgetPrepared); this.sgWidget.on("preparing", this.onWidgetPrepared);
this._sgWidget.on("ready", this._onWidgetReady); this.sgWidget.on("ready", this.onWidgetReady);
this._startWidget(); this.startWidget();
} catch (e) { } catch (e) {
console.log("Failed to construct widget", e); logger.log("Failed to construct widget", e);
this._sgWidget = null; this.sgWidget = null;
} }
} }
_startWidget() { private startWidget(): void {
this._sgWidget.prepare().then(() => { this.sgWidget.prepare().then(() => {
this.setState({ initialising: false }); this.setState({ initialising: false });
}); });
} }
_iframeRefChange = (ref) => { private iframeRefChange = (ref: HTMLIFrameElement): void => {
this.iframe = ref; this.iframe = ref;
if (ref) { if (ref) {
if (this._sgWidget) this._sgWidget.start(ref); if (this.sgWidget) this.sgWidget.start(ref);
} else { } else {
this._resetWidget(this.props); this.resetWidget(this.props);
} }
}; };
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase // eslint-disable-next-line @typescript-eslint/naming-convention
public UNSAFE_componentWillReceiveProps(nextProps: IProps): void { // eslint-disable-line camelcase
if (nextProps.app.url !== this.props.app.url) { if (nextProps.app.url !== this.props.app.url) {
this._getNewState(nextProps); this.getNewState(nextProps);
if (this.state.hasPermissionToLoad) { if (this.state.hasPermissionToLoad) {
this._resetWidget(nextProps); this.resetWidget(nextProps);
} }
} }
@ -198,7 +262,7 @@ export default class AppTile extends React.Component {
* @private * @private
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed. * @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
*/ */
async _endWidgetActions() { // widget migration dev note: async to maintain signature private async endWidgetActions(): Promise<void> { // widget migration dev note: async to maintain signature
// HACK: This is a really dirty way to ensure that Jitsi cleans up // HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media // its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/element-web/issues/7351 // stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
@ -217,27 +281,27 @@ export default class AppTile extends React.Component {
} }
// Delete the widget from the persisted store for good measure. // Delete the widget from the persisted store for good measure.
PersistedElement.destroyElement(this._persistKey); PersistedElement.destroyElement(this.persistKey);
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id);
if (this._sgWidget) this._sgWidget.stop({ forceDestroy: true }); if (this.sgWidget) this.sgWidget.stop({ forceDestroy: true });
} }
_onWidgetPrepared = () => { private onWidgetPrepared = (): void => {
this.setState({ loading: false }); this.setState({ loading: false });
}; };
_onWidgetReady = () => { private onWidgetReady = (): void => {
if (WidgetType.JITSI.matches(this.props.app.type)) { if (WidgetType.JITSI.matches(this.props.app.type)) {
this._sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {}); this.sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
} }
}; };
_onAction = payload => { private onAction = (payload): void => {
if (payload.widgetId === this.props.app.id) { if (payload.widgetId === this.props.app.id) {
switch (payload.action) { switch (payload.action) {
case 'm.sticker': case 'm.sticker':
if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) { if (this.sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
dis.dispatch({ action: 'post_sticker_message', data: payload.data }); dis.dispatch({ action: 'post_sticker_message', data: payload.data });
dis.dispatch({ action: 'stickerpicker_close' }); dis.dispatch({ action: 'stickerpicker_close' });
} else { } else {
@ -248,7 +312,7 @@ export default class AppTile extends React.Component {
} }
}; };
_grantWidgetPermission = () => { private grantWidgetPermission = (): void => {
const roomId = this.props.room.roomId; const roomId = this.props.room.roomId;
console.info("Granting permission for widget to load: " + this.props.app.eventId); console.info("Granting permission for widget to load: " + this.props.app.eventId);
const current = SettingsStore.getValue("allowedWidgets", roomId); const current = SettingsStore.getValue("allowedWidgets", roomId);
@ -258,14 +322,14 @@ export default class AppTile extends React.Component {
this.setState({ hasPermissionToLoad: true }); this.setState({ hasPermissionToLoad: true });
// Fetch a token for the integration manager, now that we're allowed to // Fetch a token for the integration manager, now that we're allowed to
this._startWidget(); this.startWidget();
}).catch(err => { }).catch(err => {
console.error(err); console.error(err);
// We don't really need to do anything about this - the user will just hit the button again. // We don't really need to do anything about this - the user will just hit the button again.
}); });
}; };
formatAppTileName() { private formatAppTileName(): string {
let appTileName = "No name"; let appTileName = "No name";
if (this.props.app.name && this.props.app.name.trim()) { if (this.props.app.name && this.props.app.name.trim()) {
appTileName = this.props.app.name.trim(); appTileName = this.props.app.name.trim();
@ -278,11 +342,11 @@ export default class AppTile extends React.Component {
* actual widget URL * actual widget URL
* @returns {bool} true If using a local version of the widget * @returns {bool} true If using a local version of the widget
*/ */
_usingLocalWidget() { private usingLocalWidget(): boolean {
return WidgetType.JITSI.matches(this.props.app.type); return WidgetType.JITSI.matches(this.props.app.type);
} }
_getTileTitle() { private getTileTitle(): JSX.Element {
const name = this.formatAppTileName(); const name = this.formatAppTileName();
const titleSpacer = <span>&nbsp;-&nbsp;</span>; const titleSpacer = <span>&nbsp;-&nbsp;</span>;
let title = ''; let title = '';
@ -300,32 +364,32 @@ export default class AppTile extends React.Component {
} }
// TODO replace with full screen interactions // TODO replace with full screen interactions
_onPopoutWidgetClick = () => { private onPopoutWidgetClick = (): void => {
// Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them // Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them
// twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop). // twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
if (WidgetType.JITSI.matches(this.props.app.type)) { if (WidgetType.JITSI.matches(this.props.app.type)) {
this._endWidgetActions().then(() => { this.endWidgetActions().then(() => {
if (this.iframe) { if (this.iframe) {
// Reload iframe // Reload iframe
this.iframe.src = this._sgWidget.embedUrl; this.iframe.src = this.sgWidget.embedUrl;
} }
}); });
} }
// Using Object.assign workaround as the following opens in a new window instead of a new tab. // Using Object.assign workaround as the following opens in a new window instead of a new tab.
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes'); // window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
Object.assign(document.createElement('a'), Object.assign(document.createElement('a'),
{ target: '_blank', href: this._sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click(); { target: '_blank', href: this.sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click();
}; };
_onContextMenuClick = () => { private onContextMenuClick = (): void => {
this.setState({ menuDisplayed: true }); this.setState({ menuDisplayed: true });
}; };
_closeContextMenu = () => { private closeContextMenu = (): void => {
this.setState({ menuDisplayed: false }); this.setState({ menuDisplayed: false });
}; };
render() { public render(): JSX.Element {
let appTileBody; let appTileBody;
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin // Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
@ -351,7 +415,7 @@ export default class AppTile extends React.Component {
<Spinner message={_t("Loading...")} /> <Spinner message={_t("Loading...")} />
</div> </div>
); );
if (this._sgWidget === null) { if (this.sgWidget === null) {
appTileBody = ( appTileBody = (
<div className={appTileBodyClass} style={appTileBodyStyles}> <div className={appTileBodyClass} style={appTileBodyStyles}>
<AppWarning errorMsg={_t("Error loading Widget")} /> <AppWarning errorMsg={_t("Error loading Widget")} />
@ -365,9 +429,9 @@ export default class AppTile extends React.Component {
<AppPermission <AppPermission
roomId={this.props.room.roomId} roomId={this.props.room.roomId}
creatorUserId={this.props.creatorUserId} creatorUserId={this.props.creatorUserId}
url={this._sgWidget.embedUrl} url={this.sgWidget.embedUrl}
isRoomEncrypted={isEncrypted} isRoomEncrypted={isEncrypted}
onPermissionGranted={this._grantWidgetPermission} onPermissionGranted={this.grantWidgetPermission}
/> />
</div> </div>
); );
@ -390,8 +454,8 @@ export default class AppTile extends React.Component {
{ this.state.loading && loadingElement } { this.state.loading && loadingElement }
<iframe <iframe
allow={iframeFeatures} allow={iframeFeatures}
ref={this._iframeRefChange} ref={this.iframeRefChange}
src={this._sgWidget.embedUrl} src={this.sgWidget.embedUrl}
allowFullScreen={true} allowFullScreen={true}
sandbox={sandboxFlags} sandbox={sandboxFlags}
/> />
@ -407,7 +471,7 @@ export default class AppTile extends React.Component {
// Also wrap the PersistedElement in a div to fix the height, otherwise // Also wrap the PersistedElement in a div to fix the height, otherwise
// AppTile's border is in the wrong place // AppTile's border is in the wrong place
appTileBody = <div className="mx_AppTile_persistedWrapper"> appTileBody = <div className="mx_AppTile_persistedWrapper">
<PersistedElement persistKey={this._persistKey}> <PersistedElement persistKey={this.persistKey}>
{ appTileBody } { appTileBody }
</PersistedElement> </PersistedElement>
</div>; </div>;
@ -429,9 +493,9 @@ export default class AppTile extends React.Component {
if (this.state.menuDisplayed) { if (this.state.menuDisplayed) {
contextMenu = ( contextMenu = (
<RoomWidgetContextMenu <RoomWidgetContextMenu
{...aboveLeftOf(this._contextMenuButton.current.getBoundingClientRect(), null)} {...aboveLeftOf(this.contextMenuButton.current.getBoundingClientRect(), null)}
app={this.props.app} app={this.props.app}
onFinished={this._closeContextMenu} onFinished={this.closeContextMenu}
showUnpin={!this.props.userWidget} showUnpin={!this.props.userWidget}
userWidget={this.props.userWidget} userWidget={this.props.userWidget}
onEditClick={this.props.onEditClick} onEditClick={this.props.onEditClick}
@ -444,21 +508,21 @@ export default class AppTile extends React.Component {
<div className={appTileClasses} id={this.props.app.id}> <div className={appTileClasses} id={this.props.app.id}>
{ this.props.showMenubar && { this.props.showMenubar &&
<div className="mx_AppTileMenuBar"> <div className="mx_AppTileMenuBar">
<span className="mx_AppTileMenuBarTitle" style={{ pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false) }}> <span className="mx_AppTileMenuBarTitle" style={{ pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : "none") }}>
{ this.props.showTitle && this._getTileTitle() } { this.props.showTitle && this.getTileTitle() }
</span> </span>
<span className="mx_AppTileMenuBarWidgets"> <span className="mx_AppTileMenuBarWidgets">
{ this.props.showPopout && <AccessibleButton { this.props.showPopout && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout" className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
title={_t('Popout widget')} title={_t('Popout widget')}
onClick={this._onPopoutWidgetClick} onClick={this.onPopoutWidgetClick}
/> } /> }
<ContextMenuButton <ContextMenuButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu" className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
label={_t("Options")} label={_t("Options")}
isExpanded={this.state.menuDisplayed} isExpanded={this.state.menuDisplayed}
inputRef={this._contextMenuButton} inputRef={this.contextMenuButton}
onClick={this._onContextMenuClick} onClick={this.onContextMenuClick}
/> />
</span> </span>
</div> } </div> }
@ -469,49 +533,3 @@ export default class AppTile extends React.Component {
</React.Fragment>; </React.Fragment>;
} }
} }
AppTile.displayName = 'AppTile';
AppTile.propTypes = {
app: PropTypes.object.isRequired,
// If room is not specified then it is an account level widget
// which bypasses permission prompts as it was added explicitly by that user
room: PropTypes.object,
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
fullWidth: PropTypes.bool,
// Optional. If set, renders a smaller view of the widget
miniMode: PropTypes.bool,
// UserId of the current user
userId: PropTypes.string.isRequired,
// UserId of the entity that added / modified the widget
creatorUserId: PropTypes.string,
waitForIframeLoad: PropTypes.bool,
showMenubar: PropTypes.bool,
// Optional onEditClickHandler (overrides default behaviour)
onEditClick: PropTypes.func,
// Optional onDeleteClickHandler (overrides default behaviour)
onDeleteClick: PropTypes.func,
// Optional onMinimiseClickHandler
onMinimiseClick: PropTypes.func,
// Optionally hide the tile title
showTitle: PropTypes.bool,
// Optionally handle minimise button pointer events (default false)
handleMinimisePointerEvents: PropTypes.bool,
// Optionally hide the popout widget icon
showPopout: PropTypes.bool,
// Is this an instance of a user widget
userWidget: PropTypes.bool,
// sets the pointer-events property on the iframe
pointerEvents: PropTypes.string,
};
AppTile.defaultProps = {
waitForIframeLoad: true,
showMenubar: true,
showTitle: true,
showPopout: true,
handleMinimisePointerEvents: false,
userWidget: false,
miniMode: false,
};

View file

@ -1,24 +1,20 @@
import React from 'react'; // eslint-disable-line no-unused-vars import React from 'react';
import PropTypes from 'prop-types';
const AppWarning = (props) => { interface IProps {
errorMsg?: string;
}
const AppWarning: React.FC<IProps> = (props) => {
return ( return (
<div className='mx_AppPermissionWarning'> <div className='mx_AppPermissionWarning'>
<div className='mx_AppPermissionWarningImage'> <div className='mx_AppPermissionWarningImage'>
<img src={require("../../../../res/img/warning.svg")} alt='' /> <img src={require("../../../../res/img/warning.svg")} alt='' />
</div> </div>
<div className='mx_AppPermissionWarningText'> <div className='mx_AppPermissionWarningText'>
<span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg }</span> <span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg || "Error" }</span>
</div> </div>
</div> </div>
); );
}; };
AppWarning.propTypes = {
errorMsg: PropTypes.string,
};
AppWarning.defaultProps = {
errorMsg: 'Error',
};
export default AppWarning; export default AppWarning;

View file

@ -17,60 +17,61 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import PropTypes from "prop-types";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
// The primary button which is styled differently and has default focus.
primaryButton: React.ReactNode;
// A node to insert into the cancel button instead of default "Cancel"
cancelButton?: React.ReactNode;
// If true, make the primary button a form submit button (input type="submit")
primaryIsSubmit?: boolean;
// onClick handler for the primary button.
onPrimaryButtonClick?: (ev: React.MouseEvent) => void;
// should there be a cancel button? default: true
hasCancel?: boolean;
// The class of the cancel button, only used if a cancel button is
// enabled
cancelButtonClass?: string;
// onClick handler for the cancel button.
onCancel?: (...args: any[]) => void;
focus?: boolean;
// disables the primary and cancel buttons
disabled?: boolean;
// disables only the primary button
primaryDisabled?: boolean;
// something to stick next to the buttons, optionally
additive?: React.ReactNode;
primaryButtonClass?: string;
}
/** /**
* Basic container for buttons in modal dialogs. * Basic container for buttons in modal dialogs.
*/ */
@replaceableComponent("views.elements.DialogButtons") @replaceableComponent("views.elements.DialogButtons")
export default class DialogButtons extends React.Component { export default class DialogButtons extends React.Component<IProps> {
static propTypes = { public static defaultProps: Partial<IProps> = {
// The primary button which is styled differently and has default focus.
primaryButton: PropTypes.node.isRequired,
// A node to insert into the cancel button instead of default "Cancel"
cancelButton: PropTypes.node,
// If true, make the primary button a form submit button (input type="submit")
primaryIsSubmit: PropTypes.bool,
// onClick handler for the primary button.
onPrimaryButtonClick: PropTypes.func,
// should there be a cancel button? default: true
hasCancel: PropTypes.bool,
// The class of the cancel button, only used if a cancel button is
// enabled
cancelButtonClass: PropTypes.node,
// onClick handler for the cancel button.
onCancel: PropTypes.func,
focus: PropTypes.bool,
// disables the primary and cancel buttons
disabled: PropTypes.bool,
// disables only the primary button
primaryDisabled: PropTypes.bool,
// something to stick next to the buttons, optionally
additive: PropTypes.element,
};
static defaultProps = {
hasCancel: true, hasCancel: true,
disabled: false, disabled: false,
}; };
_onCancelClick = () => { private onCancelClick = (event: React.MouseEvent): void => {
this.props.onCancel(); this.props.onCancel(event);
}; };
render() { public render(): JSX.Element {
let primaryButtonClassName = "mx_Dialog_primary"; let primaryButtonClassName = "mx_Dialog_primary";
if (this.props.primaryButtonClass) { if (this.props.primaryButtonClass) {
primaryButtonClassName += " " + this.props.primaryButtonClass; primaryButtonClassName += " " + this.props.primaryButtonClass;
@ -82,7 +83,7 @@ export default class DialogButtons extends React.Component {
// important: the default type is 'submit' and this button comes before the // important: the default type is 'submit' and this button comes before the
// primary in the DOM so will get form submissions unless we make it not a submit. // primary in the DOM so will get form submissions unless we make it not a submit.
type="button" type="button"
onClick={this._onCancelClick} onClick={this.onCancelClick}
className={this.props.cancelButtonClass} className={this.props.cancelButtonClass}
disabled={this.props.disabled} disabled={this.props.disabled}
> >

View file

@ -14,71 +14,73 @@ 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, createRef } from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleButton from "./AccessibleButton";
interface IProps {
className?: string;
onChange?: (value: string) => void;
onClear?: () => void;
onJoinClick?: (value: string) => void;
placeholder?: string;
showJoinButton?: boolean;
initialText?: string;
}
interface IState {
value: string;
}
@replaceableComponent("views.elements.DirectorySearchBox") @replaceableComponent("views.elements.DirectorySearchBox")
export default class DirectorySearchBox extends React.Component { export default class DirectorySearchBox extends React.Component<IProps, IState> {
constructor(props) { private input = createRef<HTMLInputElement>();
super(props);
this._collectInput = this._collectInput.bind(this);
this._onClearClick = this._onClearClick.bind(this);
this._onChange = this._onChange.bind(this);
this._onKeyUp = this._onKeyUp.bind(this);
this._onJoinButtonClick = this._onJoinButtonClick.bind(this);
this.input = null; constructor(props: IProps) {
super(props);
this.state = { this.state = {
value: this.props.initialText || '', value: this.props.initialText || '',
}; };
} }
_collectInput(e) { private onClearClick = (): void => {
this.input = e;
}
_onClearClick() {
this.setState({ value: '' }); this.setState({ value: '' });
if (this.input) { if (this.input.current) {
this.input.focus(); this.input.current.focus();
if (this.props.onClear) { if (this.props.onClear) {
this.props.onClear(); this.props.onClear();
} }
} }
} };
_onChange(ev) { private onChange = (ev: ChangeEvent<HTMLInputElement>): void => {
if (!this.input) return; if (!this.input.current) return;
this.setState({ value: ev.target.value }); this.setState({ value: ev.target.value });
if (this.props.onChange) { if (this.props.onChange) {
this.props.onChange(ev.target.value); this.props.onChange(ev.target.value);
} }
} };
_onKeyUp(ev) { private onKeyUp = (ev: React.KeyboardEvent): void => {
if (ev.key == 'Enter' && this.props.showJoinButton) { if (ev.key == 'Enter' && this.props.showJoinButton) {
if (this.props.onJoinClick) { if (this.props.onJoinClick) {
this.props.onJoinClick(this.state.value); this.props.onJoinClick(this.state.value);
} }
} }
} };
_onJoinButtonClick() { private onJoinButtonClick = (): void => {
if (this.props.onJoinClick) { if (this.props.onJoinClick) {
this.props.onJoinClick(this.state.value); this.props.onJoinClick(this.state.value);
} }
} };
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
public render(): JSX.Element {
const searchboxClasses = { const searchboxClasses = {
mx_DirectorySearchBox: true, mx_DirectorySearchBox: true,
}; };
@ -87,7 +89,7 @@ export default class DirectorySearchBox extends React.Component {
let joinButton; let joinButton;
if (this.props.showJoinButton) { if (this.props.showJoinButton) {
joinButton = <AccessibleButton className="mx_DirectorySearchBox_joinButton" joinButton = <AccessibleButton className="mx_DirectorySearchBox_joinButton"
onClick={this._onJoinButtonClick} onClick={this.onJoinButtonClick}
>{ _t("Join") }</AccessibleButton>; >{ _t("Join") }</AccessibleButton>;
} }
@ -97,24 +99,15 @@ export default class DirectorySearchBox extends React.Component {
name="dirsearch" name="dirsearch"
value={this.state.value} value={this.state.value}
className="mx_textinput_icon mx_textinput_search" className="mx_textinput_icon mx_textinput_search"
ref={this._collectInput} ref={this.input}
onChange={this._onChange} onChange={this.onChange}
onKeyUp={this._onKeyUp} onKeyUp={this.onKeyUp}
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
autoFocus autoFocus
/> />
{ joinButton } { joinButton }
<AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} /> <AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this.onClearClick} />
</div>; </div>;
} }
} }
DirectorySearchBox.propTypes = {
className: PropTypes.string,
onChange: PropTypes.func,
onClear: PropTypes.func,
onJoinClick: PropTypes.func,
placeholder: PropTypes.string,
showJoinButton: PropTypes.bool,
initialText: PropTypes.string,
};

View file

@ -16,33 +16,42 @@ 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 { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
enum Phases {
Display = "display",
Edit = "edit",
}
interface IProps {
onValueChanged?: (value: string, shouldSubmit: boolean) => void;
initialValue?: string;
label?: string;
placeholder?: string;
className?: string;
labelClassName?: string;
placeholderClassName?: string;
// Overrides blurToSubmit if true
blurToCancel?: boolean;
// Will cause onValueChanged(value, true) to fire on blur
blurToSubmit?: boolean;
editable?: boolean;
}
interface IState {
phase: Phases;
}
@replaceableComponent("views.elements.EditableText") @replaceableComponent("views.elements.EditableText")
export default class EditableText extends React.Component { export default class EditableText extends React.Component<IProps, IState> {
static propTypes = { // we track value as an JS object field rather than in React state
onValueChanged: PropTypes.func, // as React doesn't play nice with contentEditable.
initialValue: PropTypes.string, public value = '';
label: PropTypes.string, private placeholder = false;
placeholder: PropTypes.string, private editableDiv = createRef<HTMLDivElement>();
className: PropTypes.string,
labelClassName: PropTypes.string,
placeholderClassName: PropTypes.string,
// Overrides blurToSubmit if true
blurToCancel: PropTypes.bool,
// Will cause onValueChanged(value, true) to fire on blur
blurToSubmit: PropTypes.bool,
editable: PropTypes.bool,
};
static Phases = { public static defaultProps: Partial<IProps> = {
Display: "display",
Edit: "edit",
};
static defaultProps = {
onValueChanged() {}, onValueChanged() {},
initialValue: '', initialValue: '',
label: '', label: '',
@ -53,81 +62,61 @@ export default class EditableText extends React.Component {
blurToSubmit: false, blurToSubmit: false,
}; };
constructor(props) { constructor(props: IProps) {
super(props); super(props);
// we track value as an JS object field rather than in React state this.state = {
// as React doesn't play nice with contentEditable. phase: Phases.Display,
this.value = ''; };
this.placeholder = false;
this._editable_div = createRef();
} }
state = {
phase: EditableText.Phases.Display,
};
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
UNSAFE_componentWillReceiveProps(nextProps) { public UNSAFE_componentWillReceiveProps(nextProps: IProps): void {
if (nextProps.initialValue !== this.props.initialValue) { if (nextProps.initialValue !== this.props.initialValue) {
this.value = nextProps.initialValue; this.value = nextProps.initialValue;
if (this._editable_div.current) { if (this.editableDiv.current) {
this.showPlaceholder(!this.value); this.showPlaceholder(!this.value);
} }
} }
} }
componentDidMount() { public componentDidMount(): void {
this.value = this.props.initialValue; this.value = this.props.initialValue;
if (this._editable_div.current) { if (this.editableDiv.current) {
this.showPlaceholder(!this.value); this.showPlaceholder(!this.value);
} }
} }
showPlaceholder = show => { private showPlaceholder = (show: boolean): void => {
if (show) { if (show) {
this._editable_div.current.textContent = this.props.placeholder; this.editableDiv.current.textContent = this.props.placeholder;
this._editable_div.current.setAttribute("class", this.props.className this.editableDiv.current.setAttribute("class", this.props.className
+ " " + this.props.placeholderClassName); + " " + this.props.placeholderClassName);
this.placeholder = true; this.placeholder = true;
this.value = ''; this.value = '';
} else { } else {
this._editable_div.current.textContent = this.value; this.editableDiv.current.textContent = this.value;
this._editable_div.current.setAttribute("class", this.props.className); this.editableDiv.current.setAttribute("class", this.props.className);
this.placeholder = false; this.placeholder = false;
} }
}; };
getValue = () => this.value; private cancelEdit = (): void => {
setValue = value => {
this.value = value;
this.showPlaceholder(!this.value);
};
edit = () => {
this.setState({ this.setState({
phase: EditableText.Phases.Edit, phase: Phases.Display,
});
};
cancelEdit = () => {
this.setState({
phase: EditableText.Phases.Display,
}); });
this.value = this.props.initialValue; this.value = this.props.initialValue;
this.showPlaceholder(!this.value); this.showPlaceholder(!this.value);
this.onValueChanged(false); this.onValueChanged(false);
this._editable_div.current.blur(); this.editableDiv.current.blur();
}; };
onValueChanged = shouldSubmit => { private onValueChanged = (shouldSubmit: boolean): void => {
this.props.onValueChanged(this.value, shouldSubmit); this.props.onValueChanged(this.value, shouldSubmit);
}; };
onKeyDown = ev => { private onKeyDown = (ev: React.KeyboardEvent<HTMLDivElement>): void => {
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
if (this.placeholder) { if (this.placeholder) {
@ -142,13 +131,13 @@ export default class EditableText extends React.Component {
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
}; };
onKeyUp = ev => { private onKeyUp = (ev: React.KeyboardEvent<HTMLDivElement>): void => {
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); // console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
if (!ev.target.textContent) { if (!(ev.target as HTMLDivElement).textContent) {
this.showPlaceholder(true); this.showPlaceholder(true);
} else if (!this.placeholder) { } else if (!this.placeholder) {
this.value = ev.target.textContent; this.value = (ev.target as HTMLDivElement).textContent;
} }
if (ev.key === Key.ENTER) { if (ev.key === Key.ENTER) {
@ -160,22 +149,22 @@ export default class EditableText extends React.Component {
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); // console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
}; };
onClickDiv = ev => { private onClickDiv = (): void => {
if (!this.props.editable) return; if (!this.props.editable) return;
this.setState({ this.setState({
phase: EditableText.Phases.Edit, phase: Phases.Edit,
}); });
}; };
onFocus = ev => { private onFocus = (ev: React.FocusEvent<HTMLDivElement>): void => {
//ev.target.setSelectionRange(0, ev.target.textContent.length); //ev.target.setSelectionRange(0, ev.target.textContent.length);
const node = ev.target.childNodes[0]; const node = ev.target.childNodes[0];
if (node) { if (node) {
const range = document.createRange(); const range = document.createRange();
range.setStart(node, 0); range.setStart(node, 0);
range.setEnd(node, node.length); range.setEnd(node, ev.target.childNodes.length);
const sel = window.getSelection(); const sel = window.getSelection();
sel.removeAllRanges(); sel.removeAllRanges();
@ -183,11 +172,15 @@ export default class EditableText extends React.Component {
} }
}; };
onFinish = (ev, shouldSubmit) => { private onFinish = (
ev: React.KeyboardEvent<HTMLDivElement> | React.FocusEvent<HTMLDivElement>,
shouldSubmit?: boolean,
): void => {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this; const self = this;
const submit = (ev.key === Key.ENTER) || shouldSubmit; const submit = ("key" in ev && ev.key === Key.ENTER) || shouldSubmit;
this.setState({ this.setState({
phase: EditableText.Phases.Display, phase: Phases.Display,
}, () => { }, () => {
if (this.value !== this.props.initialValue) { if (this.value !== this.props.initialValue) {
self.onValueChanged(submit); self.onValueChanged(submit);
@ -195,7 +188,7 @@ export default class EditableText extends React.Component {
}); });
}; };
onBlur = ev => { private onBlur = (ev: React.FocusEvent<HTMLDivElement>): void => {
const sel = window.getSelection(); const sel = window.getSelection();
sel.removeAllRanges(); sel.removeAllRanges();
@ -208,11 +201,11 @@ export default class EditableText extends React.Component {
this.showPlaceholder(!this.value); this.showPlaceholder(!this.value);
}; };
render() { public render(): JSX.Element {
const { className, editable, initialValue, label, labelClassName } = this.props; const { className, editable, initialValue, label, labelClassName } = this.props;
let editableEl; let editableEl;
if (!editable || (this.state.phase === EditableText.Phases.Display && if (!editable || (this.state.phase === Phases.Display &&
(label || labelClassName) && !this.value) (label || labelClassName) && !this.value)
) { ) {
// show the label // show the label
@ -222,7 +215,7 @@ export default class EditableText extends React.Component {
} else { } else {
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together // show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
editableEl = <div editableEl = <div
ref={this._editable_div} ref={this.editableDiv}
contentEditable={true} contentEditable={true}
className={className} className={className}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}

View file

@ -15,9 +15,34 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from "./Spinner";
import EditableText from "./EditableText";
interface IProps {
/* callback to retrieve the initial value. */
getInitialValue?: () => Promise<string>;
/* initial value; used if getInitialValue is not given */
initialValue?: string;
/* placeholder text to use when the value is empty (and not being
* edited) */
placeholder?: string;
/* callback to update the value. Called with a single argument: the new
* value. */
onSubmit?: (value: string) => Promise<{} | void>;
/* should the input submit when focus is lost? */
blurToSubmit?: boolean;
}
interface IState {
busy: boolean;
errorString: string;
value: string;
}
/** /**
* A component which wraps an EditableText, with a spinner while updates take * A component which wraps an EditableText, with a spinner while updates take
@ -31,50 +56,51 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
* taken from the 'initialValue' property. * taken from the 'initialValue' property.
*/ */
@replaceableComponent("views.elements.EditableTextContainer") @replaceableComponent("views.elements.EditableTextContainer")
export default class EditableTextContainer extends React.Component { export default class EditableTextContainer extends React.Component<IProps, IState> {
constructor(props) { private unmounted = false;
public static defaultProps: Partial<IProps> = {
initialValue: "",
placeholder: "",
blurToSubmit: false,
onSubmit: () => { return Promise.resolve(); },
};
constructor(props: IProps) {
super(props); super(props);
this._unmounted = false;
this.state = { this.state = {
busy: false, busy: false,
errorString: null, errorString: null,
value: props.initialValue, value: props.initialValue,
}; };
this._onValueChanged = this._onValueChanged.bind(this);
} }
componentDidMount() { public async componentDidMount(): Promise<void> {
if (this.props.getInitialValue === undefined) { // use whatever was given in the initialValue property.
// use whatever was given in the initialValue property. if (this.props.getInitialValue === undefined) return;
return;
}
this.setState({ busy: true }); this.setState({ busy: true });
try {
this.props.getInitialValue().then( const initialValue = await this.props.getInitialValue();
(result) => { if (this.unmounted) return;
if (this._unmounted) { return; } this.setState({
this.setState({ busy: false,
busy: false, value: initialValue,
value: result, });
}); } catch (error) {
}, if (this.unmounted) return;
(error) => { this.setState({
if (this._unmounted) { return; } errorString: error.toString(),
this.setState({ busy: false,
errorString: error.toString(), });
busy: false, }
});
},
);
} }
componentWillUnmount() { public componentWillUnmount(): void {
this._unmounted = true; this.unmounted = true;
} }
_onValueChanged(value, shouldSubmit) { private onValueChanged = (value: string, shouldSubmit: boolean): void => {
if (!shouldSubmit) { if (!shouldSubmit) {
return; return;
} }
@ -86,38 +112,36 @@ export default class EditableTextContainer extends React.Component {
this.props.onSubmit(value).then( this.props.onSubmit(value).then(
() => { () => {
if (this._unmounted) { return; } if (this.unmounted) { return; }
this.setState({ this.setState({
busy: false, busy: false,
value: value, value: value,
}); });
}, },
(error) => { (error) => {
if (this._unmounted) { return; } if (this.unmounted) { return; }
this.setState({ this.setState({
errorString: error.toString(), errorString: error.toString(),
busy: false, busy: false,
}); });
}, },
); );
} };
render() { public render(): JSX.Element {
if (this.state.busy) { if (this.state.busy) {
const Loader = sdk.getComponent("elements.Spinner");
return ( return (
<Loader /> <Spinner />
); );
} else if (this.state.errorString) { } else if (this.state.errorString) {
return ( return (
<div className="error">{ this.state.errorString }</div> <div className="error">{ this.state.errorString }</div>
); );
} else { } else {
const EditableText = sdk.getComponent('elements.EditableText');
return ( return (
<EditableText initialValue={this.state.value} <EditableText initialValue={this.state.value}
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
onValueChanged={this._onValueChanged} onValueChanged={this.onValueChanged}
blurToSubmit={this.props.blurToSubmit} blurToSubmit={this.props.blurToSubmit}
/> />
); );
@ -125,28 +149,3 @@ export default class EditableTextContainer extends React.Component {
} }
} }
EditableTextContainer.propTypes = {
/* callback to retrieve the initial value. */
getInitialValue: PropTypes.func,
/* initial value; used if getInitialValue is not given */
initialValue: PropTypes.string,
/* placeholder text to use when the value is empty (and not being
* edited) */
placeholder: PropTypes.string,
/* callback to update the value. Called with a single argument: the new
* value. */
onSubmit: PropTypes.func,
/* should the input submit when focus is lost? */
blurToSubmit: PropTypes.bool,
};
EditableTextContainer.defaultProps = {
initialValue: "",
placeholder: "",
blurToSubmit: false,
onSubmit: function(v) {return Promise.resolve(); },
};

View file

@ -34,6 +34,7 @@ import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { normalizeWheelEvent } from "../../../utils/Mouse"; import { normalizeWheelEvent } from "../../../utils/Mouse";
import { IDialogProps } from '../dialogs/IDialogProps'; import { IDialogProps } from '../dialogs/IDialogProps';
import UIStore from '../../../stores/UIStore';
// Max scale to keep gaps around the image // Max scale to keep gaps around the image
const MAX_SCALE = 0.95; const MAX_SCALE = 0.95;
@ -44,6 +45,13 @@ const ZOOM_COEFFICIENT = 0.0025;
// If we have moved only this much we can zoom // If we have moved only this much we can zoom
const ZOOM_DISTANCE = 10; const ZOOM_DISTANCE = 10;
// Height of mx_ImageView_panel
const getPanelHeight = (): number => {
const value = getComputedStyle(document.documentElement).getPropertyValue("--image-view-panel-height");
// Return the value as a number without the unit
return parseInt(value.slice(0, value.length - 2));
};
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
src: string; // the source of the image being displayed src: string; // the source of the image being displayed
name?: string; // the main title ('name') for the image name?: string; // the main title ('name') for the image
@ -56,8 +64,15 @@ interface IProps extends IDialogProps {
// redactions, senders, timestamps etc. Other descriptors are taken from the explicit // redactions, senders, timestamps etc. Other descriptors are taken from the explicit
// properties above, which let us use lightboxes to display images which aren't associated // properties above, which let us use lightboxes to display images which aren't associated
// with events. // with events.
mxEvent: MatrixEvent; mxEvent?: MatrixEvent;
permalinkCreator: RoomPermalinkCreator; permalinkCreator?: RoomPermalinkCreator;
thumbnailInfo?: {
positionX: number;
positionY: number;
width: number;
height: number;
};
} }
interface IState { interface IState {
@ -75,13 +90,25 @@ interface IState {
export default class ImageView extends React.Component<IProps, IState> { export default class ImageView extends React.Component<IProps, IState> {
constructor(props) { constructor(props) {
super(props); super(props);
const { thumbnailInfo } = this.props;
this.state = { this.state = {
zoom: 0, zoom: 0, // We default to 0 and override this in imageLoaded once we have naturalSize
minZoom: MAX_SCALE, minZoom: MAX_SCALE,
maxZoom: MAX_SCALE, maxZoom: MAX_SCALE,
rotation: 0, rotation: 0,
translationX: 0, translationX: (
translationY: 0, thumbnailInfo?.positionX +
(thumbnailInfo?.width / 2) -
(UIStore.instance.windowWidth / 2)
) ?? 0,
translationY: (
thumbnailInfo?.positionY +
(thumbnailInfo?.height / 2) -
(UIStore.instance.windowHeight / 2) -
(getPanelHeight() / 2)
) ?? 0,
moving: false, moving: false,
contextMenuDisplayed: false, contextMenuDisplayed: false,
}; };
@ -98,6 +125,9 @@ export default class ImageView extends React.Component<IProps, IState> {
private previousX = 0; private previousX = 0;
private previousY = 0; private previousY = 0;
private animatingLoading = false;
private imageIsLoaded = false;
componentDidMount() { componentDidMount() {
// We have to use addEventListener() because the listener // We have to use addEventListener() because the listener
// needs to be passive in order to work with Chromium // needs to be passive in order to work with Chromium
@ -105,15 +135,37 @@ export default class ImageView extends React.Component<IProps, IState> {
// We want to recalculate zoom whenever the window's size changes // We want to recalculate zoom whenever the window's size changes
window.addEventListener("resize", this.recalculateZoom); window.addEventListener("resize", this.recalculateZoom);
// After the image loads for the first time we want to calculate the zoom // After the image loads for the first time we want to calculate the zoom
this.image.current.addEventListener("load", this.recalculateZoom); this.image.current.addEventListener("load", this.imageLoaded);
} }
componentWillUnmount() { componentWillUnmount() {
this.focusLock.current.removeEventListener('wheel', this.onWheel); this.focusLock.current.removeEventListener('wheel', this.onWheel);
window.removeEventListener("resize", this.recalculateZoom); window.removeEventListener("resize", this.recalculateZoom);
this.image.current.removeEventListener("load", this.recalculateZoom); this.image.current.removeEventListener("load", this.imageLoaded);
} }
private imageLoaded = () => {
// First, we calculate the zoom, so that the image has the same size as
// the thumbnail
const { thumbnailInfo } = this.props;
if (thumbnailInfo?.width) {
this.setState({ zoom: thumbnailInfo.width / this.image.current.naturalWidth });
}
// Once the zoom is set, we the image is considered loaded and we can
// start animating it into the center of the screen
this.imageIsLoaded = true;
this.animatingLoading = true;
this.setZoomAndRotation();
this.setState({
translationX: 0,
translationY: 0,
});
// Once the position is set, there is no need to animate anymore
this.animatingLoading = false;
};
private recalculateZoom = () => { private recalculateZoom = () => {
this.setZoomAndRotation(); this.setZoomAndRotation();
}; };
@ -360,16 +412,17 @@ export default class ImageView extends React.Component<IProps, IState> {
const showEventMeta = !!this.props.mxEvent; const showEventMeta = !!this.props.mxEvent;
const zoomingDisabled = this.state.maxZoom === this.state.minZoom; const zoomingDisabled = this.state.maxZoom === this.state.minZoom;
let transitionClassName;
if (this.animatingLoading) transitionClassName = "mx_ImageView_image_animatingLoading";
else if (this.state.moving || !this.imageIsLoaded) transitionClassName = "";
else transitionClassName = "mx_ImageView_image_animating";
let cursor; let cursor;
if (this.state.moving) { if (this.state.moving) cursor = "grabbing";
cursor= "grabbing"; else if (zoomingDisabled) cursor = "default";
} else if (zoomingDisabled) { else if (this.state.zoom === this.state.minZoom) cursor = "zoom-in";
cursor = "default"; else cursor = "zoom-out";
} else if (this.state.zoom === this.state.minZoom) {
cursor = "zoom-in";
} else {
cursor = "zoom-out";
}
const rotationDegrees = this.state.rotation + "deg"; const rotationDegrees = this.state.rotation + "deg";
const zoom = this.state.zoom; const zoom = this.state.zoom;
const translatePixelsX = this.state.translationX + "px"; const translatePixelsX = this.state.translationX + "px";
@ -380,7 +433,6 @@ export default class ImageView extends React.Component<IProps, IState> {
// image causing it translate in the wrong direction. // image causing it translate in the wrong direction.
const style = { const style = {
cursor: cursor, cursor: cursor,
transition: this.state.moving ? null : "transform 200ms ease 0s",
transform: `translateX(${translatePixelsX}) transform: `translateX(${translatePixelsX})
translateY(${translatePixelsY}) translateY(${translatePixelsY})
scale(${zoom}) scale(${zoom})
@ -528,7 +580,7 @@ export default class ImageView extends React.Component<IProps, IState> {
style={style} style={style}
alt={this.props.name} alt={this.props.name}
ref={this.image} ref={this.image}
className="mx_ImageView_image" className={`mx_ImageView_image ${transitionClassName}`}
draggable={true} draggable={true}
onMouseDown={this.onStartMoving} onMouseDown={this.onStartMoving}
/> />

View file

@ -16,13 +16,13 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import * as languageHandler from '../../../languageHandler'; import * as languageHandler from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from "./Spinner";
import Dropdown from "./Dropdown";
function languageMatchesSearchQuery(query, language) { function languageMatchesSearchQuery(query, language) {
if (language.label.toUpperCase().includes(query.toUpperCase())) return true; if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
@ -30,11 +30,22 @@ function languageMatchesSearchQuery(query, language) {
return false; return false;
} }
interface IProps {
className?: string;
onOptionChange: (language: string) => void;
value?: string;
disabled?: boolean;
}
interface IState {
searchQuery: string;
langs: string[];
}
@replaceableComponent("views.elements.LanguageDropdown") @replaceableComponent("views.elements.LanguageDropdown")
export default class LanguageDropdown extends React.Component { export default class LanguageDropdown extends React.Component<IProps, IState> {
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this._onSearchChange = this._onSearchChange.bind(this);
this.state = { this.state = {
searchQuery: '', searchQuery: '',
@ -42,7 +53,7 @@ export default class LanguageDropdown extends React.Component {
}; };
} }
componentDidMount() { public componentDidMount(): void {
languageHandler.getAllLanguagesFromJson().then((langs) => { languageHandler.getAllLanguagesFromJson().then((langs) => {
langs.sort(function(a, b) { langs.sort(function(a, b) {
if (a.label < b.label) return -1; if (a.label < b.label) return -1;
@ -63,20 +74,17 @@ export default class LanguageDropdown extends React.Component {
} }
} }
_onSearchChange(search) { private onSearchChange = (search: string): void => {
this.setState({ this.setState({
searchQuery: search, searchQuery: search,
}); });
} };
render() { public render(): JSX.Element {
if (this.state.langs === null) { if (this.state.langs === null) {
const Spinner = sdk.getComponent('elements.Spinner');
return <Spinner />; return <Spinner />;
} }
const Dropdown = sdk.getComponent('elements.Dropdown');
let displayedLanguages; let displayedLanguages;
if (this.state.searchQuery) { if (this.state.searchQuery) {
displayedLanguages = this.state.langs.filter((lang) => { displayedLanguages = this.state.langs.filter((lang) => {
@ -107,7 +115,7 @@ export default class LanguageDropdown extends React.Component {
id="mx_LanguageDropdown" id="mx_LanguageDropdown"
className={this.props.className} className={this.props.className}
onOptionChange={this.props.onOptionChange} onOptionChange={this.props.onOptionChange}
onSearchChange={this._onSearchChange} onSearchChange={this.onSearchChange}
searchEnabled={true} searchEnabled={true}
value={value} value={value}
label={_t("Language Dropdown")} label={_t("Language Dropdown")}
@ -118,8 +126,3 @@ export default class LanguageDropdown extends React.Component {
} }
} }
LanguageDropdown.propTypes = {
className: PropTypes.string,
onOptionChange: PropTypes.func.isRequired,
value: PropTypes.string,
};

View file

@ -15,17 +15,16 @@ 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";
class ItemRange { class ItemRange {
constructor(topCount, renderCount, bottomCount) { constructor(
this.topCount = topCount; public topCount: number,
this.renderCount = renderCount; public renderCount: number,
this.bottomCount = bottomCount; public bottomCount: number,
} ) { }
contains(range) { public contains(range: ItemRange): boolean {
// don't contain empty ranges // don't contain empty ranges
// as it will prevent clearing the list // as it will prevent clearing the list
// once it is scrolled far enough out of view // once it is scrolled far enough out of view
@ -36,7 +35,7 @@ class ItemRange {
(range.topCount + range.renderCount) <= (this.topCount + this.renderCount); (range.topCount + range.renderCount) <= (this.topCount + this.renderCount);
} }
expand(amount) { public expand(amount: number): ItemRange {
// don't expand ranges that won't render anything // don't expand ranges that won't render anything
if (this.renderCount === 0) { if (this.renderCount === 0) {
return this; return this;
@ -51,20 +50,55 @@ class ItemRange {
); );
} }
totalSize() { public totalSize(): number {
return this.topCount + this.renderCount + this.bottomCount; return this.topCount + this.renderCount + this.bottomCount;
} }
} }
interface IProps<T> {
// height in pixels of the component returned by `renderItem`
itemHeight: number;
// function to turn an element of `items` into a react component
renderItem: (item: T) => JSX.Element;
// scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s)
scrollTop: number;
// the height of the viewport this content is scrolled in
height: number;
// all items for the list. These should not be react components, see `renderItem`.
items?: T[];
// the amount of items to scroll before causing a rerender,
// should typically be less than `overflowItems` unless applying
// margins in the parent component when using multiple LazyRenderList in one viewport.
// use 0 to only rerender when items will come into view.
overflowMargin?: number;
// the amount of items to add at the top and bottom to render,
// so not every scroll of causes a rerender.
overflowItems?: number;
element?: string;
className?: string;
}
interface IState {
renderRange: ItemRange;
}
@replaceableComponent("views.elements.LazyRenderList") @replaceableComponent("views.elements.LazyRenderList")
export default class LazyRenderList extends React.Component { export default class LazyRenderList<T = any> extends React.Component<IProps<T>, IState> {
constructor(props) { public static defaultProps: Partial<IProps<unknown>> = {
overflowItems: 20,
overflowMargin: 5,
};
constructor(props: IProps<T>) {
super(props); super(props);
this.state = {}; this.state = {
renderRange: null,
};
} }
static getDerivedStateFromProps(props, state) { public static getDerivedStateFromProps(props: IProps<unknown>, state: IState): Partial<IState> {
const range = LazyRenderList.getVisibleRangeFromProps(props); const range = LazyRenderList.getVisibleRangeFromProps(props);
const intersectRange = range.expand(props.overflowMargin); const intersectRange = range.expand(props.overflowMargin);
const renderRange = range.expand(props.overflowItems); const renderRange = range.expand(props.overflowItems);
@ -77,7 +111,7 @@ export default class LazyRenderList extends React.Component {
return null; return null;
} }
static getVisibleRangeFromProps(props) { private static getVisibleRangeFromProps(props: IProps<unknown>): ItemRange {
const { items, itemHeight, scrollTop, height } = props; const { items, itemHeight, scrollTop, height } = props;
const length = items ? items.length : 0; const length = items ? items.length : 0;
const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), length); const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), length);
@ -88,7 +122,7 @@ export default class LazyRenderList extends React.Component {
return new ItemRange(topCount, renderCount, bottomCount); return new ItemRange(topCount, renderCount, bottomCount);
} }
render() { public render(): JSX.Element {
const { itemHeight, items, renderItem } = this.props; const { itemHeight, items, renderItem } = this.props;
const { renderRange } = this.state; const { renderRange } = this.state;
const { topCount, renderCount, bottomCount } = renderRange; const { topCount, renderCount, bottomCount } = renderRange;
@ -109,28 +143,3 @@ export default class LazyRenderList extends React.Component {
} }
} }
LazyRenderList.defaultProps = {
overflowItems: 20,
overflowMargin: 5,
};
LazyRenderList.propTypes = {
// height in pixels of the component returned by `renderItem`
itemHeight: PropTypes.number.isRequired,
// function to turn an element of `items` into a react component
renderItem: PropTypes.func.isRequired,
// scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s)
scrollTop: PropTypes.number.isRequired,
// the height of the viewport this content is scrolled in
height: PropTypes.number.isRequired,
// all items for the list. These should not be react components, see `renderItem`.
items: PropTypes.array,
// the amount of items to scroll before causing a rerender,
// should typically be less than `overflowItems` unless applying
// margins in the parent component when using multiple LazyRenderList in one viewport.
// use 0 to only rerender when items will come into view.
overflowMargin: PropTypes.number,
// the amount of items to add at the top and bottom to render,
// so not every scroll of causes a rerender.
overflowItems: PropTypes.number,
};

View file

@ -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) {

View file

@ -16,25 +16,26 @@ limitations under the License.
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { throttle } from "lodash"; import { throttle } from "lodash";
import ResizeObserver from 'resize-observer-polyfill';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ActionPayload } from "../../../dispatcher/payloads";
export const getPersistKey = (appId: string) => 'widget_' + appId;
// Shamelessly ripped off Modal.js. There's probably a better way // Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and // of doing reusable widgets like dialog boxes & menus where we go and
// pass in a custom control as the actual body. // pass in a custom control as the actual body.
function getContainer(containerId) { function getContainer(containerId: string): HTMLDivElement {
return document.getElementById(containerId); return document.getElementById(containerId) as HTMLDivElement;
} }
function getOrCreateContainer(containerId) { function getOrCreateContainer(containerId: string): HTMLDivElement {
let container = getContainer(containerId); let container = getContainer(containerId);
if (!container) { if (!container) {
@ -46,7 +47,19 @@ function getOrCreateContainer(containerId) {
return container; return container;
} }
/* interface IProps {
// Unique identifier for this PersistedElement instance
// Any PersistedElements with the same persistKey will use
// the same DOM container.
persistKey: string;
// z-index for the element. Defaults to 9.
zIndex?: number;
style?: React.StyleHTMLAttributes<HTMLDivElement>;
}
/**
* Class of component that renders its children in a separate ReactDOM virtual tree * Class of component that renders its children in a separate ReactDOM virtual tree
* in a container element appended to document.body. * in a container element appended to document.body.
* *
@ -58,42 +71,33 @@ function getOrCreateContainer(containerId) {
* bounding rect as the parent of PE. * bounding rect as the parent of PE.
*/ */
@replaceableComponent("views.elements.PersistedElement") @replaceableComponent("views.elements.PersistedElement")
export default class PersistedElement extends React.Component { export default class PersistedElement extends React.Component<IProps> {
static propTypes = { private resizeObserver: ResizeObserver;
// Unique identifier for this PersistedElement instance private dispatcherRef: string;
// Any PersistedElements with the same persistKey will use private childContainer: HTMLDivElement;
// the same DOM container. private child: HTMLDivElement;
persistKey: PropTypes.string.isRequired,
// z-index for the element. Defaults to 9. constructor(props: IProps) {
zIndex: PropTypes.number, super(props);
};
constructor() { this.resizeObserver = new ResizeObserver(this.repositionChild);
super();
this.collectChildContainer = this.collectChildContainer.bind(this);
this.collectChild = this.collectChild.bind(this);
this._repositionChild = this._repositionChild.bind(this);
this._onAction = this._onAction.bind(this);
this.resizeObserver = new ResizeObserver(this._repositionChild);
// Annoyingly, a resize observer is insufficient, since we also care // Annoyingly, a resize observer is insufficient, since we also care
// about when the element moves on the screen without changing its // about when the element moves on the screen without changing its
// dimensions. Doesn't look like there's a ResizeObserver equivalent // dimensions. Doesn't look like there's a ResizeObserver equivalent
// for this, so we bodge it by listening for document resize and // for this, so we bodge it by listening for document resize and
// the timeline_resize action. // the timeline_resize action.
window.addEventListener('resize', this._repositionChild); window.addEventListener('resize', this.repositionChild);
this._dispatcherRef = dis.register(this._onAction); this.dispatcherRef = dis.register(this.onAction);
} }
/** /**
* Removes the DOM elements created when a PersistedElement with the given * Removes the DOM elements created when a PersistedElement with the given
* persistKey was mounted. The DOM elements will be re-added if another * persistKey was mounted. The DOM elements will be re-added if another
* PeristedElement is mounted in the future. * PersistedElement is mounted in the future.
* *
* @param {string} persistKey Key used to uniquely identify this PersistedElement * @param {string} persistKey Key used to uniquely identify this PersistedElement
*/ */
static destroyElement(persistKey) { public static destroyElement(persistKey: string): void {
const container = getContainer('mx_persistedElement_' + persistKey); const container = getContainer('mx_persistedElement_' + persistKey);
if (container) { if (container) {
container.remove(); container.remove();
@ -104,7 +108,7 @@ export default class PersistedElement extends React.Component {
return Boolean(getContainer('mx_persistedElement_' + persistKey)); return Boolean(getContainer('mx_persistedElement_' + persistKey));
} }
collectChildContainer(ref) { private collectChildContainer = (ref: HTMLDivElement): void => {
if (this.childContainer) { if (this.childContainer) {
this.resizeObserver.unobserve(this.childContainer); this.resizeObserver.unobserve(this.childContainer);
} }
@ -112,48 +116,48 @@ export default class PersistedElement extends React.Component {
if (ref) { if (ref) {
this.resizeObserver.observe(ref); this.resizeObserver.observe(ref);
} }
} };
collectChild(ref) { private collectChild = (ref: HTMLDivElement): void => {
this.child = ref; this.child = ref;
this.updateChild(); this.updateChild();
} };
componentDidMount() { public componentDidMount(): void {
this.updateChild(); this.updateChild();
this.renderApp(); this.renderApp();
} }
componentDidUpdate() { public componentDidUpdate(): void {
this.updateChild(); this.updateChild();
this.renderApp(); this.renderApp();
} }
componentWillUnmount() { public componentWillUnmount(): void {
this.updateChildVisibility(this.child, false); this.updateChildVisibility(this.child, false);
this.resizeObserver.disconnect(); this.resizeObserver.disconnect();
window.removeEventListener('resize', this._repositionChild); window.removeEventListener('resize', this.repositionChild);
dis.unregister(this._dispatcherRef); dis.unregister(this.dispatcherRef);
} }
_onAction(payload) { private onAction = (payload: ActionPayload): void => {
if (payload.action === 'timeline_resize') { if (payload.action === 'timeline_resize') {
this._repositionChild(); this.repositionChild();
} else if (payload.action === 'logout') { } else if (payload.action === 'logout') {
PersistedElement.destroyElement(this.props.persistKey); PersistedElement.destroyElement(this.props.persistKey);
} }
} };
_repositionChild() { private repositionChild = (): void => {
this.updateChildPosition(this.child, this.childContainer); this.updateChildPosition(this.child, this.childContainer);
} };
updateChild() { private updateChild(): void {
this.updateChildPosition(this.child, this.childContainer); this.updateChildPosition(this.child, this.childContainer);
this.updateChildVisibility(this.child, true); this.updateChildVisibility(this.child, true);
} }
renderApp() { private renderApp(): void {
const content = <MatrixClientContext.Provider value={MatrixClientPeg.get()}> const content = <MatrixClientContext.Provider value={MatrixClientPeg.get()}>
<div ref={this.collectChild} style={this.props.style}> <div ref={this.collectChild} style={this.props.style}>
{ this.props.children } { this.props.children }
@ -163,12 +167,12 @@ export default class PersistedElement extends React.Component {
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey)); ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
} }
updateChildVisibility(child, visible) { private updateChildVisibility(child: HTMLDivElement, visible: boolean): void {
if (!child) return; if (!child) return;
child.style.display = visible ? 'block' : 'none'; child.style.display = visible ? 'block' : 'none';
} }
updateChildPosition = throttle((child, parent) => { private updateChildPosition = throttle((child: HTMLDivElement, parent: HTMLDivElement): void => {
if (!child || !parent) return; if (!child || !parent) return;
const parentRect = parent.getBoundingClientRect(); const parentRect = parent.getBoundingClientRect();
@ -182,9 +186,8 @@ export default class PersistedElement extends React.Component {
}); });
}, 100, { trailing: true, leading: true }); }, 100, { trailing: true, leading: true });
render() { public render(): JSX.Element {
return <div ref={this.collectChildContainer} />; return <div ref={this.collectChildContainer} />;
} }
} }
export const getPersistKey = (appId) => 'widget_' + appId;

View file

@ -17,61 +17,74 @@ limitations under the License.
import React from 'react'; import React from 'react';
import RoomViewStore from '../../../stores/RoomViewStore'; import RoomViewStore from '../../../stores/RoomViewStore';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/ActiveWidgetStore';
import WidgetUtils from '../../../utils/WidgetUtils'; import WidgetUtils from '../../../utils/WidgetUtils';
import * as sdk from '../../../index';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { EventSubscription } from 'fbemitter';
import AppTile from "./AppTile";
import { Room } from "matrix-js-sdk/src/models/room";
interface IState {
roomId: string;
persistentWidgetId: string;
}
@replaceableComponent("views.elements.PersistentApp") @replaceableComponent("views.elements.PersistentApp")
export default class PersistentApp extends React.Component { export default class PersistentApp extends React.Component<{}, IState> {
state = { private roomStoreToken: EventSubscription;
roomId: RoomViewStore.getRoomId(),
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
};
componentDidMount() { constructor() {
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); super({});
ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
MatrixClientPeg.get().on("Room.myMembership", this._onMyMembership); this.state = {
roomId: RoomViewStore.getRoomId(),
persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(),
};
} }
componentWillUnmount() { public componentDidMount(): void {
if (this._roomStoreToken) { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
this._roomStoreToken.remove(); ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate);
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
}
public componentWillUnmount(): void {
if (this.roomStoreToken) {
this.roomStoreToken.remove();
} }
ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate); ActiveWidgetStore.instance.removeListener(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate);
if (MatrixClientPeg.get()) { if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room.myMembership", this._onMyMembership); MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
} }
} }
_onRoomViewStoreUpdate = payload => { private onRoomViewStoreUpdate = (): void => {
if (RoomViewStore.getRoomId() === this.state.roomId) return; if (RoomViewStore.getRoomId() === this.state.roomId) return;
this.setState({ this.setState({
roomId: RoomViewStore.getRoomId(), roomId: RoomViewStore.getRoomId(),
}); });
}; };
_onActiveWidgetStoreUpdate = () => { private onActiveWidgetStoreUpdate = (): void => {
this.setState({ this.setState({
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(),
}); });
}; };
_onMyMembership = async (room, membership) => { private onMyMembership = async (room: Room, membership: string): Promise<void> => {
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId); const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId);
if (membership !== "join") { if (membership !== "join") {
// we're not in the room anymore - delete // we're not in the room anymore - delete
if (room.roomId === persistentWidgetInRoomId) { if (room .roomId === persistentWidgetInRoomId) {
ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId); ActiveWidgetStore.instance.destroyPersistentWidget(this.state.persistentWidgetId);
} }
} }
}; };
render() { public render(): JSX.Element {
if (this.state.persistentWidgetId) { if (this.state.persistentWidgetId) {
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId); const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId);
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId); const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
@ -83,13 +96,12 @@ export default class PersistentApp extends React.Component {
if (this.state.roomId !== persistentWidgetInRoomId && myMembership === "join") { if (this.state.roomId !== persistentWidgetInRoomId && myMembership === "join") {
// get the widget data // get the widget data
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => { const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId(); return ev.getStateKey() === ActiveWidgetStore.instance.getPersistentWidgetId();
}); });
const app = WidgetUtils.makeAppConfig( const app = WidgetUtils.makeAppConfig(
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
persistentWidgetInRoomId, appEvent.getId(), persistentWidgetInRoomId, appEvent.getId(),
); );
const AppTile = sdk.getComponent('elements.AppTile');
return <AppTile return <AppTile
key={app.id} key={app.id}
app={app} app={app}

View file

@ -15,40 +15,52 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as Roles from '../../../Roles'; import * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Field from "./Field"; import Field from "./Field";
import { Key } from "../../../Keyboard"; import { Key } from "../../../Keyboard";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
const CUSTOM_VALUE = "SELECT_VALUE_CUSTOM";
interface IProps {
value: number;
// The maximum value that can be set with the power selector
maxValue: number;
// Default user power level for the room
usersDefault: number;
// should the user be able to change the value? false by default.
disabled?: boolean;
onChange?: (value: number, powerLevelKey: string) => void;
// Optional key to pass as the second argument to `onChange`
powerLevelKey?: string;
// The name to annotate the selector with
label?: string;
}
interface IState {
levelRoleMap: {};
// List of power levels to show in the drop-down
options: number[];
customValue: number;
selectValue: number | string;
custom?: boolean;
customLevel?: number;
}
@replaceableComponent("views.elements.PowerSelector") @replaceableComponent("views.elements.PowerSelector")
export default class PowerSelector extends React.Component { export default class PowerSelector extends React.Component<IProps, IState> {
static propTypes = { public static defaultProps: Partial<IProps> = {
value: PropTypes.number.isRequired,
// The maximum value that can be set with the power selector
maxValue: PropTypes.number.isRequired,
// Default user power level for the room
usersDefault: PropTypes.number.isRequired,
// should the user be able to change the value? false by default.
disabled: PropTypes.bool,
onChange: PropTypes.func,
// Optional key to pass as the second argument to `onChange`
powerLevelKey: PropTypes.string,
// The name to annotate the selector with
label: PropTypes.string,
}
static defaultProps = {
maxValue: Infinity, maxValue: Infinity,
usersDefault: 0, usersDefault: 0,
}; };
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {
@ -62,26 +74,26 @@ export default class PowerSelector extends React.Component {
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
UNSAFE_componentWillMount() { public UNSAFE_componentWillMount(): void {
this._initStateFromProps(this.props); this.initStateFromProps(this.props);
} }
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
UNSAFE_componentWillReceiveProps(newProps) { public UNSAFE_componentWillReceiveProps(newProps: IProps): void {
this._initStateFromProps(newProps); this.initStateFromProps(newProps);
} }
_initStateFromProps(newProps) { private initStateFromProps(newProps: IProps): void {
// This needs to be done now because levelRoleMap has translated strings // This needs to be done now because levelRoleMap has translated strings
const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault); const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault);
const options = Object.keys(levelRoleMap).filter(level => { const options = Object.keys(levelRoleMap).filter(level => {
return ( return (
level === undefined || level === undefined ||
level <= newProps.maxValue || parseInt(level) <= newProps.maxValue ||
level == newProps.value parseInt(level) == newProps.value
); );
}); }).map(level => parseInt(level));
const isCustom = levelRoleMap[newProps.value] === undefined; const isCustom = levelRoleMap[newProps.value] === undefined;
@ -90,32 +102,33 @@ export default class PowerSelector extends React.Component {
options, options,
custom: isCustom, custom: isCustom,
customLevel: newProps.value, customLevel: newProps.value,
selectValue: isCustom ? "SELECT_VALUE_CUSTOM" : newProps.value, selectValue: isCustom ? CUSTOM_VALUE : newProps.value,
}); });
} }
onSelectChange = event => { private onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
const isCustom = event.target.value === "SELECT_VALUE_CUSTOM"; const isCustom = event.target.value === CUSTOM_VALUE;
if (isCustom) { if (isCustom) {
this.setState({ custom: true }); this.setState({ custom: true });
} else { } else {
this.props.onChange(event.target.value, this.props.powerLevelKey); const powerLevel = parseInt(event.target.value);
this.setState({ selectValue: event.target.value }); this.props.onChange(powerLevel, this.props.powerLevelKey);
this.setState({ selectValue: powerLevel });
} }
}; };
onCustomChange = event => { private onCustomChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ customValue: event.target.value }); this.setState({ customValue: parseInt(event.target.value) });
}; };
onCustomBlur = event => { private onCustomBlur = (event: React.FocusEvent): void => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.props.onChange(parseInt(this.state.customValue), this.props.powerLevelKey); this.props.onChange(this.state.customValue, this.props.powerLevelKey);
}; };
onCustomKeyDown = event => { private onCustomKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
if (event.key === Key.ENTER) { if (event.key === Key.ENTER) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -125,11 +138,11 @@ export default class PowerSelector extends React.Component {
// raising a dialog which causes a blur which causes a dialog which causes a blur and // raising a dialog which causes a blur which causes a dialog which causes a blur and
// so on. By not causing the onChange to be called here, we avoid the loop because we // so on. By not causing the onChange to be called here, we avoid the loop because we
// handle the onBlur safely. // handle the onBlur safely.
event.target.blur(); (event.target as HTMLInputElement).blur();
} }
}; };
render() { public render(): JSX.Element {
let picker; let picker;
const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label; const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label;
if (this.state.custom) { if (this.state.custom) {
@ -147,14 +160,14 @@ export default class PowerSelector extends React.Component {
); );
} else { } else {
// Each level must have a definition in this.state.levelRoleMap // Each level must have a definition in this.state.levelRoleMap
let options = this.state.options.map((level) => { const options = this.state.options.map((level) => {
return { return {
value: level, value: String(level),
text: Roles.textualPowerLevel(level, this.props.usersDefault), text: Roles.textualPowerLevel(level, this.props.usersDefault),
}; };
}); });
options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") }); options.push({ value: CUSTOM_VALUE, text: _t("Custom level") });
options = options.map((op) => { const optionsElements = options.map((op) => {
return <option value={op.value} key={op.value}>{ op.text }</option>; return <option value={op.value} key={op.value}>{ op.text }</option>;
}); });
@ -166,7 +179,7 @@ export default class PowerSelector extends React.Component {
value={String(this.state.selectValue)} value={String(this.state.selectValue)}
disabled={this.props.disabled} disabled={this.props.disabled}
> >
{ options } { optionsElements }
</Field> </Field>
); );
} }

View file

@ -88,7 +88,13 @@ export default class ReplyThread extends React.Component<IProps, IState> {
// could be used here for replies as well... However, the helper // could be used here for replies as well... However, the helper
// currently assumes the relation has a `rel_type`, which older replies // currently assumes the relation has a `rel_type`, which older replies
// do not, so this block is left as-is for now. // do not, so this block is left as-is for now.
const mRelatesTo = ev.getWireContent()['m.relates_to']; //
// We're prefer ev.getContent() over ev.getWireContent() to make sure
// we grab the latest edit with potentially new relations. But we also
// can't just rely on ev.getContent() by itself because historically we
// still show the reply from the original message even though the edit
// event does not include the relation reply.
const mRelatesTo = ev.getContent()['m.relates_to'] || ev.getWireContent()['m.relates_to'];
if (mRelatesTo && mRelatesTo['m.in_reply_to']) { if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
const mInReplyTo = mRelatesTo['m.in_reply_to']; const mInReplyTo = mRelatesTo['m.in_reply_to'];
if (mInReplyTo && mInReplyTo['event_id']) return mInReplyTo['event_id']; if (mInReplyTo && mInReplyTo['event_id']) return mInReplyTo['event_id'];

View file

@ -17,25 +17,34 @@
import React from 'react'; import React from 'react';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
reason?: string;
contentHtml: string;
}
interface IState {
visible: boolean;
}
@replaceableComponent("views.elements.Spoiler") @replaceableComponent("views.elements.Spoiler")
export default class Spoiler extends React.Component { export default class Spoiler extends React.Component<IProps, IState> {
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {
visible: false, visible: false,
}; };
} }
toggleVisible(e) { private toggleVisible = (e: React.MouseEvent): void => {
if (!this.state.visible) { if (!this.state.visible) {
// we are un-blurring, we don't want this click to propagate to potential child pills // we are un-blurring, we don't want this click to propagate to potential child pills
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
} }
this.setState({ visible: !this.state.visible }); this.setState({ visible: !this.state.visible });
} };
render() { public render(): JSX.Element {
const reason = this.props.reason ? ( const reason = this.props.reason ? (
<span className="mx_EventTile_spoiler_reason">{ "(" + this.props.reason + ")" }</span> <span className="mx_EventTile_spoiler_reason">{ "(" + this.props.reason + ")" }</span>
) : null; ) : null;
@ -43,7 +52,7 @@ export default class Spoiler extends React.Component {
// as such, we pass the this.props.contentHtml instead and then set the raw // as such, we pass the this.props.contentHtml instead and then set the raw
// HTML content. This is secure as the contents have already been parsed previously // HTML content. This is secure as the contents have already been parsed previously
return ( return (
<span className={"mx_EventTile_spoiler" + (this.state.visible ? " visible" : "")} onClick={this.toggleVisible.bind(this)}> <span className={"mx_EventTile_spoiler" + (this.state.visible ? " visible" : "")} onClick={this.toggleVisible}>
{ reason } { reason }
&nbsp; &nbsp;
<span className="mx_EventTile_spoiler_content" dangerouslySetInnerHTML={{ __html: this.props.contentHtml }} /> <span className="mx_EventTile_spoiler_content" dangerouslySetInnerHTML={{ __html: this.props.contentHtml }} />

View file

@ -15,40 +15,40 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { highlightBlock } from 'highlight.js'; import { highlightBlock } from 'highlight.js';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
className?: string;
children?: React.ReactNode;
}
@replaceableComponent("views.elements.SyntaxHighlight") @replaceableComponent("views.elements.SyntaxHighlight")
export default class SyntaxHighlight extends React.Component { export default class SyntaxHighlight extends React.Component<IProps> {
static propTypes = { private el: HTMLPreElement = null;
className: PropTypes.string,
children: PropTypes.node,
};
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this._ref = this._ref.bind(this);
} }
// componentDidUpdate used here for reusability // componentDidUpdate used here for reusability
componentDidUpdate() { public componentDidUpdate(): void {
if (this._el) highlightBlock(this._el); if (this.el) highlightBlock(this.el);
} }
// call componentDidUpdate because _ref is fired on initial render // call componentDidUpdate because _ref is fired on initial render
// which does not fire componentDidUpdate // which does not fire componentDidUpdate
_ref(el) { private ref = (el: HTMLPreElement): void => {
this._el = el; this.el = el;
this.componentDidUpdate(); this.componentDidUpdate();
} };
render() { public render(): JSX.Element {
const { className, children } = this.props; const { className, children } = this.props;
return <pre className={`${className} mx_SyntaxHighlight`} ref={this._ref}> return <pre className={`${className} mx_SyntaxHighlight`} ref={this.ref}>
<code>{ children }</code> <code>{ children }</code>
</pre>; </pre>;
} }
} }

View file

@ -15,42 +15,44 @@
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Tooltip from "./Tooltip";
interface IProps {
class?: string;
tooltipClass?: string;
tooltip: React.ReactNode;
tooltipProps?: {};
onClick?: (ev?: React.MouseEvent) => void;
}
interface IState {
hover: boolean;
}
@replaceableComponent("views.elements.TextWithTooltip") @replaceableComponent("views.elements.TextWithTooltip")
export default class TextWithTooltip extends React.Component { export default class TextWithTooltip extends React.Component<IProps, IState> {
static propTypes = { constructor(props: IProps) {
class: PropTypes.string, super(props);
tooltipClass: PropTypes.string,
tooltip: PropTypes.node.isRequired,
tooltipProps: PropTypes.object,
};
constructor() {
super();
this.state = { this.state = {
hover: false, hover: false,
}; };
} }
onMouseOver = () => { private onMouseOver = (): void => {
this.setState({ hover: true }); this.setState({ hover: true });
}; };
onMouseLeave = () => { private onMouseLeave = (): void => {
this.setState({ hover: false }); this.setState({ hover: false });
}; };
render() { public render(): JSX.Element {
const Tooltip = sdk.getComponent("elements.Tooltip");
const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props; const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props;
return ( return (
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}> <span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} onClick={this.props.onClick} className={className}>
{ children } { children }
{ this.state.hover && <Tooltip { this.state.hover && <Tooltip
{...tooltipProps} {...tooltipProps}

View file

@ -15,20 +15,20 @@ 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";
import QRCode from "../QRCode"; import QRCode from "../QRCode";
import { QRCodeData } from "matrix-js-sdk/src/crypto/verification/QRCode";
interface IProps {
qrCodeData: QRCodeData;
}
@replaceableComponent("views.elements.crypto.VerificationQRCode") @replaceableComponent("views.elements.crypto.VerificationQRCode")
export default class VerificationQRCode extends React.PureComponent { export default class VerificationQRCode extends React.PureComponent<IProps> {
static propTypes = { public render(): JSX.Element {
qrCodeData: PropTypes.object.isRequired,
};
render() {
return ( return (
<QRCode <QRCode
data={[{ data: this.props.qrCodeData.buffer, mode: 'byte' }]} data={[{ data: this.props.qrCodeData.getBuffer(), mode: 'byte' }]}
className="mx_VerificationQRCode" className="mx_VerificationQRCode"
width={196} /> width={196} />
); );

View file

@ -15,107 +15,112 @@ limitations under the License.
*/ */
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import * as HtmlUtils from '../../../HtmlUtils'; import * as HtmlUtils from '../../../HtmlUtils';
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils'; import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
import { formatTime } from '../../../DateUtils'; import { formatTime } from '../../../DateUtils';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { pillifyLinks, unmountPills } from '../../../utils/pillify'; import { pillifyLinks, unmountPills } from '../../../utils/pillify';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import classNames from 'classnames'; import classNames from 'classnames';
import RedactedBody from "./RedactedBody"; import RedactedBody from "./RedactedBody";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleButton from "../elements/AccessibleButton";
import ConfirmAndWaitRedactDialog from "../dialogs/ConfirmAndWaitRedactDialog";
import ViewSource from "../../structures/ViewSource";
function getReplacedContent(event) { function getReplacedContent(event) {
const originalContent = event.getOriginalContent(); const originalContent = event.getOriginalContent();
return originalContent["m.new_content"] || originalContent; return originalContent["m.new_content"] || originalContent;
} }
@replaceableComponent("views.messages.EditHistoryMessage") interface IProps {
export default class EditHistoryMessage extends React.PureComponent { // the message event being edited
static propTypes = { mxEvent: MatrixEvent;
// the message event being edited previousEdit?: MatrixEvent;
mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired, isBaseEvent?: boolean;
previousEdit: PropTypes.instanceOf(MatrixEvent), isTwelveHour?: boolean;
isBaseEvent: PropTypes.bool, }
};
constructor(props) { interface IState {
canRedact: boolean;
sendStatus: EventStatus;
}
@replaceableComponent("views.messages.EditHistoryMessage")
export default class EditHistoryMessage extends React.PureComponent<IProps, IState> {
private content = createRef<HTMLDivElement>();
private pills: Element[] = [];
constructor(props: IProps) {
super(props); super(props);
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const { userId } = cli.credentials; const { userId } = cli.credentials;
const event = this.props.mxEvent; const event = this.props.mxEvent;
const room = cli.getRoom(event.getRoomId()); const room = cli.getRoom(event.getRoomId());
if (event.localRedactionEvent()) { if (event.localRedactionEvent()) {
event.localRedactionEvent().on("status", this._onAssociatedStatusChanged); event.localRedactionEvent().on("status", this.onAssociatedStatusChanged);
} }
const canRedact = room.currentState.maySendRedactionForEvent(event, userId); const canRedact = room.currentState.maySendRedactionForEvent(event, userId);
this.state = { canRedact, sendStatus: event.getAssociatedStatus() }; this.state = { canRedact, sendStatus: event.getAssociatedStatus() };
this._content = createRef();
this._pills = [];
} }
_onAssociatedStatusChanged = () => { private onAssociatedStatusChanged = (): void => {
this.setState({ sendStatus: this.props.mxEvent.getAssociatedStatus() }); this.setState({ sendStatus: this.props.mxEvent.getAssociatedStatus() });
}; };
_onRedactClick = async () => { private onRedactClick = async (): Promise<void> => {
const event = this.props.mxEvent; const event = this.props.mxEvent;
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const ConfirmAndWaitRedactDialog = sdk.getComponent("dialogs.ConfirmAndWaitRedactDialog");
Modal.createTrackedDialog('Confirm Redact Dialog', 'Edit history', ConfirmAndWaitRedactDialog, { Modal.createTrackedDialog('Confirm Redact Dialog', 'Edit history', ConfirmAndWaitRedactDialog, {
redact: () => cli.redactEvent(event.getRoomId(), event.getId()), redact: () => cli.redactEvent(event.getRoomId(), event.getId()),
}, 'mx_Dialog_confirmredact'); }, 'mx_Dialog_confirmredact');
}; };
_onViewSourceClick = () => { private onViewSourceClick = (): void => {
const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Event Source', 'Edit history', ViewSource, { Modal.createTrackedDialog('View Event Source', 'Edit history', ViewSource, {
mxEvent: this.props.mxEvent, mxEvent: this.props.mxEvent,
}, 'mx_Dialog_viewsource'); }, 'mx_Dialog_viewsource');
}; };
pillifyLinks() { private pillifyLinks(): void {
// not present for redacted events // not present for redacted events
if (this._content.current) { if (this.content.current) {
pillifyLinks(this._content.current.children, this.props.mxEvent, this._pills); pillifyLinks(this.content.current.children, this.props.mxEvent, this.pills);
} }
} }
componentDidMount() { public componentDidMount(): void {
this.pillifyLinks(); this.pillifyLinks();
} }
componentWillUnmount() { public componentWillUnmount(): void {
unmountPills(this._pills); unmountPills(this.pills);
const event = this.props.mxEvent; const event = this.props.mxEvent;
if (event.localRedactionEvent()) { if (event.localRedactionEvent()) {
event.localRedactionEvent().off("status", this._onAssociatedStatusChanged); event.localRedactionEvent().off("status", this.onAssociatedStatusChanged);
} }
} }
componentDidUpdate() { public componentDidUpdate(): void {
this.pillifyLinks(); this.pillifyLinks();
} }
_renderActionBar() { private renderActionBar(): JSX.Element {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
// hide the button when already redacted // hide the button when already redacted
let redactButton; let redactButton;
if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent && this.state.canRedact) { if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent && this.state.canRedact) {
redactButton = ( redactButton = (
<AccessibleButton onClick={this._onRedactClick}> <AccessibleButton onClick={this.onRedactClick}>
{ _t("Remove") } { _t("Remove") }
</AccessibleButton> </AccessibleButton>
); );
} }
const viewSourceButton = ( const viewSourceButton = (
<AccessibleButton onClick={this._onViewSourceClick}> <AccessibleButton onClick={this.onViewSourceClick}>
{ _t("View Source") } { _t("View Source") }
</AccessibleButton> </AccessibleButton>
); );
@ -128,7 +133,7 @@ export default class EditHistoryMessage extends React.PureComponent {
); );
} }
render() { public render(): JSX.Element {
const { mxEvent } = this.props; const { mxEvent } = this.props;
const content = getReplacedContent(mxEvent); const content = getReplacedContent(mxEvent);
let contentContainer; let contentContainer;
@ -139,18 +144,22 @@ export default class EditHistoryMessage extends React.PureComponent {
if (this.props.previousEdit) { if (this.props.previousEdit) {
contentElements = editBodyDiffToHtml(getReplacedContent(this.props.previousEdit), content); contentElements = editBodyDiffToHtml(getReplacedContent(this.props.previousEdit), content);
} else { } else {
contentElements = HtmlUtils.bodyToHtml(content, null, { stripReplyFallback: true }); contentElements = HtmlUtils.bodyToHtml(
content,
null,
{ stripReplyFallback: true, returnString: false },
);
} }
if (mxEvent.getContent().msgtype === "m.emote") { if (mxEvent.getContent().msgtype === "m.emote") {
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
contentContainer = ( contentContainer = (
<div className="mx_EventTile_content" ref={this._content}>*&nbsp; <div className="mx_EventTile_content" ref={this.content}>*&nbsp;
<span className="mx_MEmoteBody_sender">{ name }</span> <span className="mx_MEmoteBody_sender">{ name }</span>
&nbsp;{ contentElements } &nbsp;{ contentElements }
</div> </div>
); );
} else { } else {
contentContainer = <div className="mx_EventTile_content" ref={this._content}>{ contentElements }</div>; contentContainer = <div className="mx_EventTile_content" ref={this.content}>{ contentElements }</div>;
} }
} }
@ -167,7 +176,7 @@ export default class EditHistoryMessage extends React.PureComponent {
<div className="mx_EventTile_line"> <div className="mx_EventTile_line">
<span className="mx_MessageTimestamp">{ timestamp }</span> <span className="mx_MessageTimestamp">{ timestamp }</span>
{ contentContainer } { contentContainer }
{ this._renderActionBar() } { this.renderActionBar() }
</div> </div>
</div> </div>
</li> </li>

View file

@ -29,6 +29,8 @@ import { IBodyProps } from "./IBodyProps";
import { FileDownloader } from "../../../utils/FileDownloader"; import { FileDownloader } from "../../../utils/FileDownloader";
import TextWithTooltip from "../elements/TextWithTooltip"; import TextWithTooltip from "../elements/TextWithTooltip";
import { logger } from "matrix-js-sdk/src/logger";
export let DOWNLOAD_ICON_URL; // cached copy of the download.svg asset for the sandboxed iframe later on export let DOWNLOAD_ICON_URL; // cached copy of the download.svg asset for the sandboxed iframe later on
async function cacheDownloadIcon() { async function cacheDownloadIcon() {
@ -283,7 +285,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
if (["application/pdf"].includes(fileType) && !fileTooBig) { if (["application/pdf"].includes(fileType) && !fileTooBig) {
// We want to force a download on this type, so use an onClick handler. // We want to force a download on this type, so use an onClick handler.
downloadProps["onClick"] = (e) => { downloadProps["onClick"] = (e) => {
console.log(`Downloading ${fileType} as blob (unencrypted)`); logger.log(`Downloading ${fileType} as blob (unencrypted)`);
// Avoid letting the <a> do its thing // Avoid letting the <a> do its thing
e.preventDefault(); e.preventDefault();

Some files were not shown because too many files have changed in this diff Show more