Merge remote-tracking branch 'upstream/develop' into task/dialogs-ts
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
commit
d3e340e7ce
194 changed files with 3881 additions and 17100 deletions
2
.github/workflows/layered-build.yaml
vendored
2
.github/workflows/layered-build.yaml
vendored
|
@ -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
|
||||||
|
|
2
.github/workflows/typecheck.yaml
vendored
2
.github/workflows/typecheck.yaml
vendored
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -538,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();
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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', '',
|
||||||
|
|
|
@ -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
|
||||||
|
|
10
src/Login.ts
10
src/Login.ts
|
@ -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`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 + ')');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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");
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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()}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(", "),
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -30,6 +30,8 @@ import { IDialogProps } from "./IDialogProps";
|
||||||
import { IGeneratedSas, ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
|
import { IGeneratedSas, ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
|
||||||
import { VerificationBase } from "matrix-js-sdk/src/crypto/verification/Base";
|
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;
|
||||||
const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 2;
|
const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 2;
|
||||||
|
@ -61,7 +63,7 @@ export default class IncomingSasDialog extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
let phase = PHASE_START;
|
let phase = PHASE_START;
|
||||||
if (this.props.verifier.hasBeenCancelled) {
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +115,7 @@ export default class IncomingSasDialog extends React.Component<IProps, IState> {
|
||||||
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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -97,13 +97,13 @@ const LeaveRoomsPicker = ({ space, spaceChildren, roomsToLeave, setRoomsToLeave
|
||||||
definitions={[
|
definitions={[
|
||||||
{
|
{
|
||||||
value: RoomsToLeave.None,
|
value: RoomsToLeave.None,
|
||||||
label: _t("Don't leave any"),
|
label: _t("Don't leave any rooms"),
|
||||||
}, {
|
}, {
|
||||||
value: RoomsToLeave.All,
|
value: RoomsToLeave.All,
|
||||||
label: _t("Leave all rooms and spaces"),
|
label: _t("Leave all rooms"),
|
||||||
}, {
|
}, {
|
||||||
value: RoomsToLeave.Specific,
|
value: RoomsToLeave.Specific,
|
||||||
label: _t("Leave specific rooms and spaces"),
|
label: _t("Leave some rooms"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
@ -166,11 +166,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>,
|
||||||
}) }
|
}) }
|
||||||
|
|
||||||
{ rejoinWarning }
|
{ rejoinWarning }
|
||||||
|
{ rejoinWarning && (<> </>) }
|
||||||
|
{ spaceChildren.length > 0 && _t("Would you like to leave the rooms in this space?") }
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{ spaceChildren.length > 0 && <LeaveRoomsPicker
|
{ spaceChildren.length > 0 && <LeaveRoomsPicker
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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") }
|
||||||
</>}
|
</>}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -55,7 +57,7 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent<I
|
||||||
|
|
||||||
private onPermissionSelection(allowed: boolean): void {
|
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,
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { IKeyBackupInfo, IKeyBackupRestoreResult } from "matrix-js-sdk/src/crypt
|
||||||
import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api";
|
import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api";
|
||||||
import * as sdk from '../../../../index';
|
import * as sdk from '../../../../index';
|
||||||
import { IDialogProps } from "../IDialogProps";
|
import { IDialogProps } from "../IDialogProps";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
enum RestoreType {
|
enum RestoreType {
|
||||||
Passphrase = "passphrase",
|
Passphrase = "passphrase",
|
||||||
|
@ -160,7 +161,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
||||||
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,
|
||||||
|
@ -194,7 +195,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
||||||
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,
|
||||||
|
@ -226,7 +227,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
} 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,
|
||||||
|
@ -248,7 +249,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("restoreWithCachedKey failed:", e);
|
logger.log("restoreWithCachedKey failed:", e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -262,7 +263,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
||||||
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,
|
||||||
|
@ -270,7 +271,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
||||||
|
|
||||||
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,
|
||||||
});
|
});
|
||||||
|
@ -287,7 +288,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
||||||
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,
|
||||||
|
|
|
@ -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.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.getWidgetPersistence(this.props.app.id)) {
|
||||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
ActiveWidgetStore.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.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> - </span>;
|
const titleSpacer = <span> - </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,
|
|
||||||
};
|
|
|
@ -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;
|
|
@ -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}
|
||||||
>
|
>
|
|
@ -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,
|
|
||||||
};
|
|
|
@ -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";
|
||||||
|
|
||||||
@replaceableComponent("views.elements.EditableText")
|
enum Phases {
|
||||||
export default class EditableText extends React.Component {
|
Display = "display",
|
||||||
static propTypes = {
|
Edit = "edit",
|
||||||
onValueChanged: PropTypes.func,
|
}
|
||||||
initialValue: PropTypes.string,
|
|
||||||
label: PropTypes.string,
|
interface IProps {
|
||||||
placeholder: PropTypes.string,
|
onValueChanged?: (value: string, shouldSubmit: boolean) => void;
|
||||||
className: PropTypes.string,
|
initialValue?: string;
|
||||||
labelClassName: PropTypes.string,
|
label?: string;
|
||||||
placeholderClassName: PropTypes.string,
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
labelClassName?: string;
|
||||||
|
placeholderClassName?: string;
|
||||||
// Overrides blurToSubmit if true
|
// Overrides blurToSubmit if true
|
||||||
blurToCancel: PropTypes.bool,
|
blurToCancel?: boolean;
|
||||||
// Will cause onValueChanged(value, true) to fire on blur
|
// Will cause onValueChanged(value, true) to fire on blur
|
||||||
blurToSubmit: PropTypes.bool,
|
blurToSubmit?: boolean;
|
||||||
editable: PropTypes.bool,
|
editable?: boolean;
|
||||||
};
|
}
|
||||||
|
|
||||||
static Phases = {
|
interface IState {
|
||||||
Display: "display",
|
phase: Phases;
|
||||||
Edit: "edit",
|
}
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
@replaceableComponent("views.elements.EditableText")
|
||||||
|
export default class EditableText extends React.Component<IProps, IState> {
|
||||||
|
// we track value as an JS object field rather than in React state
|
||||||
|
// as React doesn't play nice with contentEditable.
|
||||||
|
public value = '';
|
||||||
|
private placeholder = false;
|
||||||
|
private editableDiv = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
public static defaultProps: Partial<IProps> = {
|
||||||
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}
|
|
@ -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.
|
||||||
return;
|
if (this.props.getInitialValue === undefined) 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: result,
|
value: initialValue,
|
||||||
});
|
});
|
||||||
},
|
} catch (error) {
|
||||||
(error) => {
|
if (this.unmounted) return;
|
||||||
if (this._unmounted) { return; }
|
|
||||||
this.setState({
|
this.setState({
|
||||||
errorString: error.toString(),
|
errorString: error.toString(),
|
||||||
busy: false,
|
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(); },
|
|
||||||
};
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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,
|
|
||||||
};
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.LazyRenderList")
|
interface IProps<T> {
|
||||||
export default class LazyRenderList extends React.Component {
|
// height in pixels of the component returned by `renderItem`
|
||||||
constructor(props) {
|
itemHeight: number;
|
||||||
super(props);
|
// 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;
|
||||||
|
|
||||||
this.state = {};
|
element?: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
interface IState {
|
||||||
|
renderRange: ItemRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.elements.LazyRenderList")
|
||||||
|
export default class LazyRenderList<T = any> extends React.Component<IProps<T>, IState> {
|
||||||
|
public static defaultProps: Partial<IProps<unknown>> = {
|
||||||
|
overflowItems: 20,
|
||||||
|
overflowMargin: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: IProps<T>) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
renderRange: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
|
||||||
};
|
|
|
@ -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;
|
|
|
@ -19,47 +19,60 @@ import React from 'react';
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
import ActiveWidgetStore 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;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({});
|
||||||
|
|
||||||
|
this.state = {
|
||||||
roomId: RoomViewStore.getRoomId(),
|
roomId: RoomViewStore.getRoomId(),
|
||||||
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
|
||||||
ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
|
|
||||||
MatrixClientPeg.get().on("Room.myMembership", this._onMyMembership);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentDidMount(): void {
|
||||||
if (this._roomStoreToken) {
|
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||||
this._roomStoreToken.remove();
|
ActiveWidgetStore.on('update', this.onActiveWidgetStoreUpdate);
|
||||||
|
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
|
||||||
}
|
}
|
||||||
ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate);
|
|
||||||
|
public componentWillUnmount(): void {
|
||||||
|
if (this.roomStoreToken) {
|
||||||
|
this.roomStoreToken.remove();
|
||||||
|
}
|
||||||
|
ActiveWidgetStore.removeListener('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.getPersistentWidgetId(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onMyMembership = async (room, membership) => {
|
private onMyMembership = async (room: Room, membership: string): Promise<void> => {
|
||||||
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
const persistentWidgetInRoomId = ActiveWidgetStore.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
|
||||||
|
@ -69,7 +82,7 @@ export default class PersistentApp extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
if (this.state.persistentWidgetId) {
|
if (this.state.persistentWidgetId) {
|
||||||
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
||||||
|
|
||||||
|
@ -89,7 +102,6 @@ export default class PersistentApp extends React.Component {
|
||||||
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}
|
|
@ -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";
|
||||||
|
|
||||||
@replaceableComponent("views.elements.PowerSelector")
|
const CUSTOM_VALUE = "SELECT_VALUE_CUSTOM";
|
||||||
export default class PowerSelector extends React.Component {
|
|
||||||
static propTypes = {
|
interface IProps {
|
||||||
value: PropTypes.number.isRequired,
|
value: number;
|
||||||
// The maximum value that can be set with the power selector
|
// The maximum value that can be set with the power selector
|
||||||
maxValue: PropTypes.number.isRequired,
|
maxValue: number;
|
||||||
|
|
||||||
// Default user power level for the room
|
// Default user power level for the room
|
||||||
usersDefault: PropTypes.number.isRequired,
|
usersDefault: number;
|
||||||
|
|
||||||
// should the user be able to change the value? false by default.
|
// should the user be able to change the value? false by default.
|
||||||
disabled: PropTypes.bool,
|
disabled?: boolean;
|
||||||
onChange: PropTypes.func,
|
onChange?: (value: number, powerLevelKey: string) => void;
|
||||||
|
|
||||||
// Optional key to pass as the second argument to `onChange`
|
// Optional key to pass as the second argument to `onChange`
|
||||||
powerLevelKey: PropTypes.string,
|
powerLevelKey?: string;
|
||||||
|
|
||||||
// The name to annotate the selector with
|
// The name to annotate the selector with
|
||||||
label: PropTypes.string,
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
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")
|
||||||
|
export default class PowerSelector extends React.Component<IProps, IState> {
|
||||||
|
public static defaultProps: Partial<IProps> = {
|
||||||
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -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'];
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
||||||
<span className="mx_EventTile_spoiler_content" dangerouslySetInnerHTML={{ __html: this.props.contentHtml }} />
|
<span className="mx_EventTile_spoiler_content" dangerouslySetInnerHTML={{ __html: this.props.contentHtml }} />
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
|
@ -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} />
|
||||||
);
|
);
|
|
@ -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();
|
||||||
|
|
|
@ -117,6 +117,17 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
params.fileSize = content.info.size;
|
params.fileSize = content.info.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.image.current) {
|
||||||
|
const clientRect = this.image.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
params.thumbnailInfo = {
|
||||||
|
width: clientRect.width,
|
||||||
|
height: clientRect.height,
|
||||||
|
positionX: clientRect.x,
|
||||||
|
positionY: clientRect.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,6 +27,8 @@ import { IMediaEventContent } from "../../../customisations/models/IMediaEventCo
|
||||||
import { IBodyProps } from "./IBodyProps";
|
import { IBodyProps } from "./IBodyProps";
|
||||||
import MFileBody from "./MFileBody";
|
import MFileBody from "./MFileBody";
|
||||||
|
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
decryptedUrl?: string;
|
decryptedUrl?: string;
|
||||||
decryptedThumbnailUrl?: string;
|
decryptedThumbnailUrl?: string;
|
||||||
|
@ -152,7 +154,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
||||||
try {
|
try {
|
||||||
const thumbnailUrl = await this.props.mediaEventHelper.thumbnailUrl.value;
|
const thumbnailUrl = await this.props.mediaEventHelper.thumbnailUrl.value;
|
||||||
if (autoplay) {
|
if (autoplay) {
|
||||||
console.log("Preloading video");
|
logger.log("Preloading video");
|
||||||
this.setState({
|
this.setState({
|
||||||
decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value,
|
decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value,
|
||||||
decryptedThumbnailUrl: thumbnailUrl,
|
decryptedThumbnailUrl: thumbnailUrl,
|
||||||
|
@ -160,7 +162,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
||||||
});
|
});
|
||||||
this.props.onHeightChanged();
|
this.props.onHeightChanged();
|
||||||
} else {
|
} else {
|
||||||
console.log("NOT preloading video");
|
logger.log("NOT preloading video");
|
||||||
const content = this.props.mxEvent.getContent<IMediaEventContent>();
|
const content = this.props.mxEvent.getContent<IMediaEventContent>();
|
||||||
this.setState({
|
this.setState({
|
||||||
// For Chrome and Electron, we need to set some non-empty `src` to
|
// For Chrome and Electron, we need to set some non-empty `src` to
|
||||||
|
|
|
@ -71,6 +71,8 @@ import UIStore from "../../../stores/UIStore";
|
||||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
import SpaceStore from "../../../stores/SpaceStore";
|
import SpaceStore from "../../../stores/SpaceStore";
|
||||||
|
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
export interface IDevice {
|
export interface IDevice {
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
ambiguous?: boolean;
|
ambiguous?: boolean;
|
||||||
|
@ -557,7 +559,7 @@ const RoomKickButton: React.FC<IBaseProps> = ({ member, startUpdating, stopUpdat
|
||||||
cli.kick(member.roomId, member.userId, reason || undefined).then(() => {
|
cli.kick(member.roomId, member.userId, reason || undefined).then(() => {
|
||||||
// NO-OP; rely on the m.room.member event coming down else we could
|
// NO-OP; rely on the m.room.member event coming down else we could
|
||||||
// get out of sync if we force setState here!
|
// get out of sync if we force setState here!
|
||||||
console.log("Kick success");
|
logger.log("Kick success");
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
console.error("Kick error: " + err);
|
console.error("Kick error: " + err);
|
||||||
Modal.createTrackedDialog('Failed to kick', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to kick', '', ErrorDialog, {
|
||||||
|
@ -684,7 +686,7 @@ const BanToggleButton: React.FC<IBaseProps> = ({ member, startUpdating, stopUpda
|
||||||
promise.then(() => {
|
promise.then(() => {
|
||||||
// NO-OP; rely on the m.room.member event coming down else we could
|
// NO-OP; rely on the m.room.member event coming down else we could
|
||||||
// get out of sync if we force setState here!
|
// get out of sync if we force setState here!
|
||||||
console.log("Ban success");
|
logger.log("Ban success");
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
console.error("Ban error: " + err);
|
console.error("Ban error: " + err);
|
||||||
Modal.createTrackedDialog('Failed to ban user', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to ban user', '', ErrorDialog, {
|
||||||
|
@ -757,7 +759,7 @@ const MuteToggleButton: React.FC<IBaseRoomProps> = ({ member, room, powerLevels,
|
||||||
cli.setPowerLevel(roomId, target, level, powerLevelEvent).then(() => {
|
cli.setPowerLevel(roomId, target, level, powerLevelEvent).then(() => {
|
||||||
// NO-OP; rely on the m.room.member event coming down else we could
|
// NO-OP; rely on the m.room.member event coming down else we could
|
||||||
// get out of sync if we force setState here!
|
// get out of sync if we force setState here!
|
||||||
console.log("Mute toggle success");
|
logger.log("Mute toggle success");
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
console.error("Mute error: " + err);
|
console.error("Mute error: " + err);
|
||||||
Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, {
|
||||||
|
@ -917,7 +919,7 @@ const GroupAdminToolsSection: React.FC<{
|
||||||
_t('Failed to withdraw invitation') :
|
_t('Failed to withdraw invitation') :
|
||||||
_t('Failed to remove user from community'),
|
_t('Failed to remove user from community'),
|
||||||
});
|
});
|
||||||
console.log(e);
|
logger.log(e);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
stopUpdating();
|
stopUpdating();
|
||||||
});
|
});
|
||||||
|
@ -1052,8 +1054,7 @@ const PowerLevelEditor: React.FC<{
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
|
const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
|
||||||
const onPowerChange = useCallback(async (powerLevelStr: string) => {
|
const onPowerChange = useCallback(async (powerLevel: number) => {
|
||||||
const powerLevel = parseInt(powerLevelStr, 10);
|
|
||||||
setSelectedPowerLevel(powerLevel);
|
setSelectedPowerLevel(powerLevel);
|
||||||
|
|
||||||
const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
|
const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
|
||||||
|
@ -1061,7 +1062,7 @@ const PowerLevelEditor: React.FC<{
|
||||||
function() {
|
function() {
|
||||||
// NO-OP; rely on the m.room.member event coming down else we could
|
// NO-OP; rely on the m.room.member event coming down else we could
|
||||||
// get out of sync if we force setState here!
|
// get out of sync if we force setState here!
|
||||||
console.log("Power change success");
|
logger.log("Power change success");
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
console.error("Failed to change power level " + err);
|
console.error("Failed to change power level " + err);
|
||||||
Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, {
|
||||||
|
|
|
@ -28,7 +28,7 @@ import { SAS } from "matrix-js-sdk/src/crypto/verification/SAS";
|
||||||
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
|
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import E2EIcon from "../rooms/E2EIcon";
|
import E2EIcon, { E2EState } from "../rooms/E2EIcon";
|
||||||
import { Phase } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
import { Phase } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
@ -189,7 +189,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
||||||
// Element Web doesn't support scanning yet, so assume here we're the client being scanned.
|
// Element Web doesn't support scanning yet, so assume here we're the client being scanned.
|
||||||
body = <React.Fragment>
|
body = <React.Fragment>
|
||||||
<p>{ description }</p>
|
<p>{ description }</p>
|
||||||
<E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
|
<E2EIcon isUser={true} status={E2EState.Verified} size={128} hideTooltip={true} />
|
||||||
<div className="mx_VerificationPanel_reciprocateButtons">
|
<div className="mx_VerificationPanel_reciprocateButtons">
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind="danger"
|
kind="danger"
|
||||||
|
@ -252,7 +252,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
||||||
<div className="mx_UserInfo_container mx_VerificationPanel_verified_section">
|
<div className="mx_UserInfo_container mx_VerificationPanel_verified_section">
|
||||||
<h3>{ _t("Verified") }</h3>
|
<h3>{ _t("Verified") }</h3>
|
||||||
<p>{ description }</p>
|
<p>{ description }</p>
|
||||||
<E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
|
<E2EIcon isUser={true} status={E2EState.Verified} size={128} hideTooltip={true} />
|
||||||
{ text ? <p>{ text }</p> : null }
|
{ text ? <p>{ text }</p> : null }
|
||||||
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
|
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
|
||||||
{ _t("Got it") }
|
{ _t("Got it") }
|
||||||
|
|
|
@ -97,7 +97,6 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
||||||
<AppTile
|
<AppTile
|
||||||
app={app}
|
app={app}
|
||||||
fullWidth
|
fullWidth
|
||||||
show
|
|
||||||
showMenubar={false}
|
showMenubar={false}
|
||||||
room={room}
|
room={room}
|
||||||
userId={cli.getUserId()}
|
userId={cli.getUserId()}
|
||||||
|
|
|
@ -15,27 +15,43 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import * as sdk from "../../../index";
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { mediaFromMxc } from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import AvatarSetting from "../settings/AvatarSetting";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
roomId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
originalDisplayName: string;
|
||||||
|
displayName: string;
|
||||||
|
originalAvatarUrl: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
avatarFile: File;
|
||||||
|
originalTopic: string;
|
||||||
|
topic: string;
|
||||||
|
enableProfileSave: boolean;
|
||||||
|
canSetName: boolean;
|
||||||
|
canSetTopic: boolean;
|
||||||
|
canSetAvatar: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Merge with ProfileSettings?
|
// TODO: Merge with ProfileSettings?
|
||||||
@replaceableComponent("views.room_settings.RoomProfileSettings")
|
@replaceableComponent("views.room_settings.RoomProfileSettings")
|
||||||
export default class RoomProfileSettings extends React.Component {
|
export default class RoomProfileSettings extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private avatarUpload = createRef<HTMLInputElement>();
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const room = client.getRoom(props.roomId);
|
const room = client.getRoom(props.roomId);
|
||||||
if (!room) throw new Error("Expected a room for ID: ", props.roomId);
|
if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`);
|
||||||
|
|
||||||
const avatarEvent = room.currentState.getStateEvents("m.room.avatar", "");
|
const avatarEvent = room.currentState.getStateEvents("m.room.avatar", "");
|
||||||
let avatarUrl = avatarEvent && avatarEvent.getContent() ? avatarEvent.getContent()["url"] : null;
|
let avatarUrl = avatarEvent && avatarEvent.getContent() ? avatarEvent.getContent()["url"] : null;
|
||||||
|
@ -60,17 +76,15 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
canSetTopic: room.currentState.maySendStateEvent('m.room.topic', client.getUserId()),
|
canSetTopic: room.currentState.maySendStateEvent('m.room.topic', client.getUserId()),
|
||||||
canSetAvatar: room.currentState.maySendStateEvent('m.room.avatar', client.getUserId()),
|
canSetAvatar: room.currentState.maySendStateEvent('m.room.avatar', client.getUserId()),
|
||||||
};
|
};
|
||||||
|
|
||||||
this._avatarUpload = createRef();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_uploadAvatar = () => {
|
private uploadAvatar = (): void => {
|
||||||
this._avatarUpload.current.click();
|
this.avatarUpload.current.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
_removeAvatar = () => {
|
private removeAvatar = (): void => {
|
||||||
// clear file upload field so same file can be selected
|
// clear file upload field so same file can be selected
|
||||||
this._avatarUpload.current.value = "";
|
this.avatarUpload.current.value = "";
|
||||||
this.setState({
|
this.setState({
|
||||||
avatarUrl: null,
|
avatarUrl: null,
|
||||||
avatarFile: null,
|
avatarFile: null,
|
||||||
|
@ -78,7 +92,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_cancelProfileChanges = async (e) => {
|
private cancelProfileChanges = async (e: React.MouseEvent): Promise<void> => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -92,7 +106,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_saveProfile = async (e) => {
|
private saveProfile = async (e: React.FormEvent): Promise<void> => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -100,35 +114,46 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
this.setState({ enableProfileSave: false });
|
this.setState({ enableProfileSave: false });
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const newState = {};
|
|
||||||
|
let originalDisplayName: string;
|
||||||
|
let avatarUrl: string;
|
||||||
|
let originalAvatarUrl: string;
|
||||||
|
let originalTopic: string;
|
||||||
|
let avatarFile: File;
|
||||||
|
|
||||||
// TODO: What do we do about errors?
|
// TODO: What do we do about errors?
|
||||||
const displayName = this.state.displayName.trim();
|
const displayName = this.state.displayName.trim();
|
||||||
if (this.state.originalDisplayName !== this.state.displayName) {
|
if (this.state.originalDisplayName !== this.state.displayName) {
|
||||||
await client.setRoomName(this.props.roomId, displayName);
|
await client.setRoomName(this.props.roomId, displayName);
|
||||||
newState.originalDisplayName = displayName;
|
originalDisplayName = displayName;
|
||||||
newState.displayName = displayName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.avatarFile) {
|
if (this.state.avatarFile) {
|
||||||
const uri = await client.uploadContent(this.state.avatarFile);
|
const uri = await client.uploadContent(this.state.avatarFile);
|
||||||
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', { url: uri }, '');
|
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', { url: uri }, '');
|
||||||
newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
|
avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
|
||||||
newState.originalAvatarUrl = newState.avatarUrl;
|
originalAvatarUrl = avatarUrl;
|
||||||
newState.avatarFile = null;
|
avatarFile = null;
|
||||||
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
|
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
|
||||||
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {}, '');
|
await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {}, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.originalTopic !== this.state.topic) {
|
if (this.state.originalTopic !== this.state.topic) {
|
||||||
await client.setRoomTopic(this.props.roomId, this.state.topic);
|
await client.setRoomTopic(this.props.roomId, this.state.topic);
|
||||||
newState.originalTopic = this.state.topic;
|
originalTopic = this.state.topic;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(newState);
|
this.setState({
|
||||||
|
originalAvatarUrl,
|
||||||
|
avatarUrl,
|
||||||
|
originalDisplayName,
|
||||||
|
originalTopic,
|
||||||
|
displayName,
|
||||||
|
avatarFile,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onDisplayNameChanged = (e) => {
|
private onDisplayNameChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({ displayName: e.target.value });
|
this.setState({ displayName: e.target.value });
|
||||||
if (this.state.originalDisplayName === e.target.value) {
|
if (this.state.originalDisplayName === e.target.value) {
|
||||||
this.setState({ enableProfileSave: false });
|
this.setState({ enableProfileSave: false });
|
||||||
|
@ -137,7 +162,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onTopicChanged = (e) => {
|
private onTopicChanged = (e: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
||||||
this.setState({ topic: e.target.value });
|
this.setState({ topic: e.target.value });
|
||||||
if (this.state.originalTopic === e.target.value) {
|
if (this.state.originalTopic === e.target.value) {
|
||||||
this.setState({ enableProfileSave: false });
|
this.setState({ enableProfileSave: false });
|
||||||
|
@ -146,7 +171,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onAvatarChanged = (e) => {
|
private onAvatarChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
if (!e.target.files || !e.target.files.length) {
|
if (!e.target.files || !e.target.files.length) {
|
||||||
this.setState({
|
this.setState({
|
||||||
avatarUrl: this.state.originalAvatarUrl,
|
avatarUrl: this.state.originalAvatarUrl,
|
||||||
|
@ -160,7 +185,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (ev) => {
|
reader.onload = (ev) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
avatarUrl: ev.target.result,
|
avatarUrl: String(ev.target.result),
|
||||||
avatarFile: file,
|
avatarFile: file,
|
||||||
enableProfileSave: true,
|
enableProfileSave: true,
|
||||||
});
|
});
|
||||||
|
@ -168,10 +193,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
|
|
||||||
|
|
||||||
let profileSettingsButtons;
|
let profileSettingsButtons;
|
||||||
if (
|
if (
|
||||||
this.state.canSetName ||
|
this.state.canSetName ||
|
||||||
|
@ -181,14 +203,14 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
profileSettingsButtons = (
|
profileSettingsButtons = (
|
||||||
<div className="mx_ProfileSettings_buttons">
|
<div className="mx_ProfileSettings_buttons">
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this._cancelProfileChanges}
|
onClick={this.cancelProfileChanges}
|
||||||
kind="link"
|
kind="link"
|
||||||
disabled={!this.state.enableProfileSave}
|
disabled={!this.state.enableProfileSave}
|
||||||
>
|
>
|
||||||
{ _t("Cancel") }
|
{ _t("Cancel") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this._saveProfile}
|
onClick={this.saveProfile}
|
||||||
kind="primary"
|
kind="primary"
|
||||||
disabled={!this.state.enableProfileSave}
|
disabled={!this.state.enableProfileSave}
|
||||||
>
|
>
|
||||||
|
@ -200,16 +222,16 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={this._saveProfile}
|
onSubmit={this.saveProfile}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
noValidate={true}
|
noValidate={true}
|
||||||
className="mx_ProfileSettings_profileForm"
|
className="mx_ProfileSettings_profileForm"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
ref={this._avatarUpload}
|
ref={this.avatarUpload}
|
||||||
className="mx_ProfileSettings_avatarUpload"
|
className="mx_ProfileSettings_avatarUpload"
|
||||||
onChange={this._onAvatarChanged}
|
onChange={this.onAvatarChanged}
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
/>
|
/>
|
||||||
<div className="mx_ProfileSettings_profile">
|
<div className="mx_ProfileSettings_profile">
|
||||||
|
@ -219,7 +241,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
type="text"
|
type="text"
|
||||||
value={this.state.displayName}
|
value={this.state.displayName}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onChange={this._onDisplayNameChanged}
|
onChange={this.onDisplayNameChanged}
|
||||||
disabled={!this.state.canSetName}
|
disabled={!this.state.canSetName}
|
||||||
/>
|
/>
|
||||||
<Field
|
<Field
|
||||||
|
@ -230,7 +252,7 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
type="text"
|
type="text"
|
||||||
value={this.state.topic}
|
value={this.state.topic}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onChange={this._onTopicChanged}
|
onChange={this.onTopicChanged}
|
||||||
element="textarea"
|
element="textarea"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -238,8 +260,8 @@ export default class RoomProfileSettings extends React.Component {
|
||||||
avatarUrl={this.state.avatarUrl}
|
avatarUrl={this.state.avatarUrl}
|
||||||
avatarName={this.state.displayName || this.props.roomId}
|
avatarName={this.state.displayName || this.props.roomId}
|
||||||
avatarAltText={_t("Room avatar")}
|
avatarAltText={_t("Room avatar")}
|
||||||
uploadAvatar={this.state.canSetAvatar ? this._uploadAvatar : undefined}
|
uploadAvatar={this.state.canSetAvatar ? this.uploadAvatar : undefined}
|
||||||
removeAvatar={this.state.canSetAvatar ? this._removeAvatar : undefined} />
|
removeAvatar={this.state.canSetAvatar ? this.removeAvatar : undefined} />
|
||||||
</div>
|
</div>
|
||||||
{ profileSettingsButtons }
|
{ profileSettingsButtons }
|
||||||
</form>
|
</form>
|
|
@ -18,8 +18,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from "../../../index";
|
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
@ -27,21 +25,22 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import SettingsFlag from "../elements/SettingsFlag";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
room: Room;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.room_settings.UrlPreviewSettings")
|
@replaceableComponent("views.room_settings.UrlPreviewSettings")
|
||||||
export default class UrlPreviewSettings extends React.Component {
|
export default class UrlPreviewSettings extends React.Component<IProps> {
|
||||||
static propTypes = {
|
private onClickUserSettings = (e: React.MouseEvent): void => {
|
||||||
room: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
_onClickUserSettings = (e) => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
dis.fire(Action.ViewUserSettings);
|
dis.fire(Action.ViewUserSettings);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
|
||||||
const roomId = this.props.room.roomId;
|
const roomId = this.props.room.roomId;
|
||||||
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);
|
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);
|
||||||
|
|
||||||
|
@ -54,18 +53,18 @@ export default class UrlPreviewSettings extends React.Component {
|
||||||
if (accountEnabled) {
|
if (accountEnabled) {
|
||||||
previewsForAccount = (
|
previewsForAccount = (
|
||||||
_t("You have <a>enabled</a> URL previews by default.", {}, {
|
_t("You have <a>enabled</a> URL previews by default.", {}, {
|
||||||
'a': (sub)=><a onClick={this._onClickUserSettings} href=''>{ sub }</a>,
|
'a': (sub)=><a onClick={this.onClickUserSettings} href=''>{ sub }</a>,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
previewsForAccount = (
|
previewsForAccount = (
|
||||||
_t("You have <a>disabled</a> URL previews by default.", {}, {
|
_t("You have <a>disabled</a> URL previews by default.", {}, {
|
||||||
'a': (sub)=><a onClick={this._onClickUserSettings} href=''>{ sub }</a>,
|
'a': (sub)=><a onClick={this.onClickUserSettings} href=''>{ sub }</a>,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
|
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, SettingLevel.ROOM)) {
|
||||||
previewsForRoom = (
|
previewsForRoom = (
|
||||||
<label>
|
<label>
|
||||||
<SettingsFlag
|
<SettingsFlag
|
|
@ -16,7 +16,6 @@ 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 { Resizable } from "re-resizable";
|
import { Resizable } from "re-resizable";
|
||||||
|
|
||||||
|
@ -26,8 +25,6 @@ import * as sdk from '../../../index';
|
||||||
import * as ScalarMessaging from '../../../ScalarMessaging';
|
import * as ScalarMessaging from '../../../ScalarMessaging';
|
||||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||||
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
|
||||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||||
import ResizeHandle from "../elements/ResizeHandle";
|
import ResizeHandle from "../elements/ResizeHandle";
|
||||||
import Resizer from "../../../resizer/resizer";
|
import Resizer from "../../../resizer/resizer";
|
||||||
|
@ -37,60 +34,74 @@ import { clamp, percentageOf, percentageWithin } from "../../../utils/numbers";
|
||||||
import { useStateCallback } from "../../../hooks/useStateCallback";
|
import { useStateCallback } from "../../../hooks/useStateCallback";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import UIStore from "../../../stores/UIStore";
|
import UIStore from "../../../stores/UIStore";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { IApp } from "../../../stores/WidgetStore";
|
||||||
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
userId: string;
|
||||||
|
room: Room;
|
||||||
|
resizeNotifier: ResizeNotifier;
|
||||||
|
showApps?: boolean; // Should apps be rendered
|
||||||
|
maxHeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
apps: IApp[];
|
||||||
|
resizingVertical: boolean; // true when changing the height of the apps drawer
|
||||||
|
resizingHorizontal: boolean; // true when chagning the distribution of the width between widgets
|
||||||
|
resizing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.AppsDrawer")
|
@replaceableComponent("views.rooms.AppsDrawer")
|
||||||
export default class AppsDrawer extends React.Component {
|
export default class AppsDrawer extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private resizeContainer: HTMLDivElement;
|
||||||
userId: PropTypes.string.isRequired,
|
private resizer: Resizer;
|
||||||
room: PropTypes.object.isRequired,
|
private dispatcherRef: string;
|
||||||
resizeNotifier: PropTypes.instanceOf(ResizeNotifier).isRequired,
|
public static defaultProps: Partial<IProps> = {
|
||||||
showApps: PropTypes.bool, // Should apps be rendered
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
showApps: true,
|
showApps: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
apps: this._getApps(),
|
apps: this.getApps(),
|
||||||
resizingVertical: false, // true when changing the height of the apps drawer
|
resizingVertical: false,
|
||||||
resizingHorizontal: false, // true when chagning the distribution of the width between widgets
|
resizingHorizontal: false,
|
||||||
|
resizing: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._resizeContainer = null;
|
this.resizer = this.createResizer();
|
||||||
this.resizer = this._createResizer();
|
|
||||||
|
|
||||||
this.props.resizeNotifier.on("isResizing", this.onIsResizing);
|
this.props.resizeNotifier.on("isResizing", this.onIsResizing);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
ScalarMessaging.startListening();
|
ScalarMessaging.startListening();
|
||||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
|
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps);
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
ScalarMessaging.stopListening();
|
ScalarMessaging.stopListening();
|
||||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
|
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps);
|
||||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||||
if (this._resizeContainer) {
|
if (this.resizeContainer) {
|
||||||
this.resizer.detach();
|
this.resizer.detach();
|
||||||
}
|
}
|
||||||
this.props.resizeNotifier.off("isResizing", this.onIsResizing);
|
this.props.resizeNotifier.off("isResizing", this.onIsResizing);
|
||||||
}
|
}
|
||||||
|
|
||||||
onIsResizing = (resizing) => {
|
private onIsResizing = (resizing: boolean): void => {
|
||||||
// This one is the vertical, ie. change height of apps drawer
|
// This one is the vertical, ie. change height of apps drawer
|
||||||
this.setState({ resizingVertical: resizing });
|
this.setState({ resizingVertical: resizing });
|
||||||
if (!resizing) {
|
if (!resizing) {
|
||||||
this._relaxResizer();
|
this.relaxResizer();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_createResizer() {
|
private createResizer(): Resizer {
|
||||||
// This is the horizontal one, changing the distribution of the width between the app tiles
|
// This is the horizontal one, changing the distribution of the width between the app tiles
|
||||||
// (ie. a vertical resize handle because, the handle itself is vertical...)
|
// (ie. a vertical resize handle because, the handle itself is vertical...)
|
||||||
const classNames = {
|
const classNames = {
|
||||||
|
@ -100,11 +111,11 @@ export default class AppsDrawer extends React.Component {
|
||||||
};
|
};
|
||||||
const collapseConfig = {
|
const collapseConfig = {
|
||||||
onResizeStart: () => {
|
onResizeStart: () => {
|
||||||
this._resizeContainer.classList.add("mx_AppsDrawer_resizing");
|
this.resizeContainer.classList.add("mx_AppsDrawer_resizing");
|
||||||
this.setState({ resizingHorizontal: true });
|
this.setState({ resizingHorizontal: true });
|
||||||
},
|
},
|
||||||
onResizeStop: () => {
|
onResizeStop: () => {
|
||||||
this._resizeContainer.classList.remove("mx_AppsDrawer_resizing");
|
this.resizeContainer.classList.remove("mx_AppsDrawer_resizing");
|
||||||
WidgetLayoutStore.instance.setResizerDistributions(
|
WidgetLayoutStore.instance.setResizerDistributions(
|
||||||
this.props.room, Container.Top,
|
this.props.room, Container.Top,
|
||||||
this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
|
this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
|
||||||
|
@ -113,13 +124,13 @@ export default class AppsDrawer extends React.Component {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// pass a truthy container for now, we won't call attach until we update it
|
// pass a truthy container for now, we won't call attach until we update it
|
||||||
const resizer = new Resizer({}, PercentageDistributor, collapseConfig);
|
const resizer = new Resizer(null, PercentageDistributor, collapseConfig);
|
||||||
resizer.setClassNames(classNames);
|
resizer.setClassNames(classNames);
|
||||||
return resizer;
|
return resizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
_collectResizer = (ref) => {
|
private collectResizer = (ref: HTMLDivElement): void => {
|
||||||
if (this._resizeContainer) {
|
if (this.resizeContainer) {
|
||||||
this.resizer.detach();
|
this.resizer.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,22 +138,22 @@ export default class AppsDrawer extends React.Component {
|
||||||
this.resizer.container = ref;
|
this.resizer.container = ref;
|
||||||
this.resizer.attach();
|
this.resizer.attach();
|
||||||
}
|
}
|
||||||
this._resizeContainer = ref;
|
this.resizeContainer = ref;
|
||||||
this._loadResizerPreferences();
|
this.loadResizerPreferences();
|
||||||
};
|
};
|
||||||
|
|
||||||
_getAppsHash = (apps) => apps.map(app => app.id).join("~");
|
private getAppsHash = (apps: IApp[]): string => apps.map(app => app.id).join("~");
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
||||||
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
|
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
|
||||||
// Room has changed, update apps
|
// Room has changed, update apps
|
||||||
this._updateApps();
|
this.updateApps();
|
||||||
} else if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
|
} else if (this.getAppsHash(this.state.apps) !== this.getAppsHash(prevState.apps)) {
|
||||||
this._loadResizerPreferences();
|
this.loadResizerPreferences();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_relaxResizer = () => {
|
private relaxResizer = (): void => {
|
||||||
const distributors = this.resizer.getDistributors();
|
const distributors = this.resizer.getDistributors();
|
||||||
|
|
||||||
// relax all items if they had any overconstrained flexboxes
|
// relax all items if they had any overconstrained flexboxes
|
||||||
|
@ -150,7 +161,7 @@ export default class AppsDrawer extends React.Component {
|
||||||
distributors.forEach(d => d.finish());
|
distributors.forEach(d => d.finish());
|
||||||
};
|
};
|
||||||
|
|
||||||
_loadResizerPreferences = () => {
|
private loadResizerPreferences = (): void => {
|
||||||
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
|
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
|
||||||
if (this.state.apps && (this.state.apps.length - 1) === distributions.length) {
|
if (this.state.apps && (this.state.apps.length - 1) === distributions.length) {
|
||||||
distributions.forEach((size, i) => {
|
distributions.forEach((size, i) => {
|
||||||
|
@ -168,11 +179,11 @@ export default class AppsDrawer extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
isResizing() {
|
private isResizing(): boolean {
|
||||||
return this.state.resizingVertical || this.state.resizingHorizontal;
|
return this.state.resizingVertical || this.state.resizingHorizontal;
|
||||||
}
|
}
|
||||||
|
|
||||||
onAction = (action) => {
|
private onAction = (action: ActionPayload): void => {
|
||||||
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
||||||
switch (action.action) {
|
switch (action.action) {
|
||||||
case 'appsDrawer':
|
case 'appsDrawer':
|
||||||
|
@ -190,23 +201,15 @@ export default class AppsDrawer extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_getApps = () => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
|
private getApps = (): IApp[] => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
|
||||||
|
|
||||||
_updateApps = () => {
|
private updateApps = (): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
apps: this._getApps(),
|
apps: this.getApps(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_launchManageIntegrations() {
|
public render(): JSX.Element {
|
||||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
|
||||||
IntegrationManagers.sharedInstance().openAll();
|
|
||||||
} else {
|
|
||||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (!this.props.showApps) return <div />;
|
if (!this.props.showApps) return <div />;
|
||||||
|
|
||||||
const apps = this.state.apps.map((app, index, arr) => {
|
const apps = this.state.apps.map((app, index, arr) => {
|
||||||
|
@ -257,7 +260,7 @@ export default class AppsDrawer extends React.Component {
|
||||||
className="mx_AppsContainer_resizer"
|
className="mx_AppsContainer_resizer"
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
>
|
>
|
||||||
<div className="mx_AppsContainer" ref={this._collectResizer}>
|
<div className="mx_AppsContainer" ref={this.collectResizer}>
|
||||||
{ apps.map((app, i) => {
|
{ apps.map((app, i) => {
|
||||||
if (i < 1) return app;
|
if (i < 1) return app;
|
||||||
return <React.Fragment key={app.key}>
|
return <React.Fragment key={app.key}>
|
||||||
|
@ -273,7 +276,18 @@ export default class AppsDrawer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const PersistentVResizer = ({
|
interface IPersistentResizerProps {
|
||||||
|
room: Room;
|
||||||
|
minHeight: number;
|
||||||
|
maxHeight: number;
|
||||||
|
className: string;
|
||||||
|
handleWrapperClass: string;
|
||||||
|
handleClass: string;
|
||||||
|
resizeNotifier: ResizeNotifier;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
|
||||||
room,
|
room,
|
||||||
minHeight,
|
minHeight,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
|
@ -303,7 +317,7 @@ const PersistentVResizer = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
return <Resizable
|
return <Resizable
|
||||||
size={{ height: Math.min(height, maxHeight) }}
|
size={{ height: Math.min(height, maxHeight), width: null }}
|
||||||
minHeight={minHeight}
|
minHeight={minHeight}
|
||||||
maxHeight={maxHeight}
|
maxHeight={maxHeight}
|
||||||
onResizeStart={() => {
|
onResizeStart={() => {
|
|
@ -16,41 +16,51 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import Tooltip from "../elements/Tooltip";
|
import Tooltip from "../elements/Tooltip";
|
||||||
|
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||||
|
|
||||||
export const E2E_STATE = {
|
export enum E2EState {
|
||||||
VERIFIED: "verified",
|
Verified = "verified",
|
||||||
WARNING: "warning",
|
Warning = "warning",
|
||||||
UNKNOWN: "unknown",
|
Unknown = "unknown",
|
||||||
NORMAL: "normal",
|
Normal = "normal",
|
||||||
UNAUTHENTICATED: "unauthenticated",
|
Unauthenticated = "unauthenticated",
|
||||||
|
}
|
||||||
|
|
||||||
|
const crossSigningUserTitles: { [key in E2EState]?: string } = {
|
||||||
|
[E2EState.Warning]: _td("This user has not verified all of their sessions."),
|
||||||
|
[E2EState.Normal]: _td("You have not verified this user."),
|
||||||
|
[E2EState.Verified]: _td("You have verified this user. This user has verified all of their sessions."),
|
||||||
|
};
|
||||||
|
const crossSigningRoomTitles: { [key in E2EState]?: string } = {
|
||||||
|
[E2EState.Warning]: _td("Someone is using an unknown session"),
|
||||||
|
[E2EState.Normal]: _td("This room is end-to-end encrypted"),
|
||||||
|
[E2EState.Verified]: _td("Everyone in this room is verified"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const crossSigningUserTitles = {
|
interface IProps {
|
||||||
[E2E_STATE.WARNING]: _td("This user has not verified all of their sessions."),
|
isUser?: boolean;
|
||||||
[E2E_STATE.NORMAL]: _td("You have not verified this user."),
|
status?: E2EState | E2EStatus;
|
||||||
[E2E_STATE.VERIFIED]: _td("You have verified this user. This user has verified all of their sessions."),
|
className?: string;
|
||||||
};
|
size?: number;
|
||||||
const crossSigningRoomTitles = {
|
onClick?: () => void;
|
||||||
[E2E_STATE.WARNING]: _td("Someone is using an unknown session"),
|
hideTooltip?: boolean;
|
||||||
[E2E_STATE.NORMAL]: _td("This room is end-to-end encrypted"),
|
bordered?: boolean;
|
||||||
[E2E_STATE.VERIFIED]: _td("Everyone in this room is verified"),
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const E2EIcon = ({ isUser, status, className, size, onClick, hideTooltip, bordered }) => {
|
const E2EIcon: React.FC<IProps> = ({ isUser, status, className, size, onClick, hideTooltip, bordered }) => {
|
||||||
const [hover, setHover] = useState(false);
|
const [hover, setHover] = useState(false);
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
mx_E2EIcon: true,
|
mx_E2EIcon: true,
|
||||||
mx_E2EIcon_bordered: bordered,
|
mx_E2EIcon_bordered: bordered,
|
||||||
mx_E2EIcon_warning: status === E2E_STATE.WARNING,
|
mx_E2EIcon_warning: status === E2EState.Warning,
|
||||||
mx_E2EIcon_normal: status === E2E_STATE.NORMAL,
|
mx_E2EIcon_normal: status === E2EState.Normal,
|
||||||
mx_E2EIcon_verified: status === E2E_STATE.VERIFIED,
|
mx_E2EIcon_verified: status === E2EState.Verified,
|
||||||
}, className);
|
}, className);
|
||||||
|
|
||||||
let e2eTitle;
|
let e2eTitle;
|
||||||
|
@ -92,12 +102,4 @@ const E2EIcon = ({ isUser, status, className, size, onClick, hideTooltip, border
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
E2EIcon.propTypes = {
|
|
||||||
isUser: PropTypes.bool,
|
|
||||||
status: PropTypes.oneOf(Object.values(E2E_STATE)),
|
|
||||||
className: PropTypes.string,
|
|
||||||
size: PropTypes.number,
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default E2EIcon;
|
export default E2EIcon;
|
|
@ -27,7 +27,7 @@ import { findEditableEvent } from '../../../utils/EventUtils';
|
||||||
import { parseEvent } from '../../../editor/deserialize';
|
import { parseEvent } from '../../../editor/deserialize';
|
||||||
import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts';
|
import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts';
|
||||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||||
import BasicMessageComposer from "./BasicMessageComposer";
|
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
@ -42,6 +42,9 @@ import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
||||||
const html = mxEvent.getContent().formatted_body;
|
const html = mxEvent.getContent().formatted_body;
|
||||||
|
@ -307,7 +310,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
description: errText,
|
description: errText,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log("Command success.");
|
logger.log("Command success.");
|
||||||
if (messageContent) return messageContent;
|
if (messageContent) return messageContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -315,6 +318,14 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
|
||||||
private sendEdit = async (): Promise<void> => {
|
private sendEdit = async (): Promise<void> => {
|
||||||
const startTime = CountlyAnalytics.getTimestamp();
|
const startTime = CountlyAnalytics.getTimestamp();
|
||||||
const editedEvent = this.props.editState.getEvent();
|
const editedEvent = this.props.editState.getEvent();
|
||||||
|
|
||||||
|
// Replace emoticon at the end of the message
|
||||||
|
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
||||||
|
const caret = this.editorRef.current?.getCaret();
|
||||||
|
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||||
|
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
||||||
|
}
|
||||||
|
|
||||||
const editContent = createEditContent(this.model, editedEvent);
|
const editContent = createEditContent(this.model, editedEvent);
|
||||||
const newContent = editContent["m.new_content"];
|
const newContent = editContent["m.new_content"];
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ import React from 'react';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import { _td } from '../../../languageHandler';
|
import { _td } from '../../../languageHandler';
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import E2EIcon from './E2EIcon';
|
import E2EIcon, { E2EState } from './E2EIcon';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import BaseAvatar from '../avatars/BaseAvatar';
|
import BaseAvatar from '../avatars/BaseAvatar';
|
||||||
import PresenceLabel from "./PresenceLabel";
|
import PresenceLabel from "./PresenceLabel";
|
||||||
|
@ -75,7 +75,7 @@ interface IProps {
|
||||||
suppressOnHover?: boolean;
|
suppressOnHover?: boolean;
|
||||||
showPresence?: boolean;
|
showPresence?: boolean;
|
||||||
subtextLabel?: string;
|
subtextLabel?: string;
|
||||||
e2eStatus?: string;
|
e2eStatus?: E2EState;
|
||||||
powerStatus?: PowerStatus;
|
powerStatus?: PowerStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ import { formatTime } from "../../../DateUtils";
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import { ALL_RULE_TYPES } from "../../../mjolnir/BanList";
|
import { ALL_RULE_TYPES } from "../../../mjolnir/BanList";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import { E2E_STATE } from "./E2EIcon";
|
import { E2EState } from "./E2EIcon";
|
||||||
import { toRem } from "../../../utils/units";
|
import { toRem } from "../../../utils/units";
|
||||||
import { WidgetType } from "../../../widgets/WidgetType";
|
import { WidgetType } from "../../../widgets/WidgetType";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
|
@ -521,7 +521,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const thread = this.state.thread;
|
const thread = this.state.thread;
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||||
if (!thread || this.props.showThreadInfo === false) {
|
if (!thread || this.props.showThreadInfo === false || thread.length <= 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -605,7 +605,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
if (encryptionInfo.mismatchedSender) {
|
if (encryptionInfo.mismatchedSender) {
|
||||||
// something definitely wrong is going on here
|
// something definitely wrong is going on here
|
||||||
this.setState({
|
this.setState({
|
||||||
verified: E2E_STATE.WARNING,
|
verified: E2EState.Warning,
|
||||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -613,7 +613,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
if (!userTrust.isCrossSigningVerified()) {
|
if (!userTrust.isCrossSigningVerified()) {
|
||||||
// user is not verified, so default to everything is normal
|
// user is not verified, so default to everything is normal
|
||||||
this.setState({
|
this.setState({
|
||||||
verified: E2E_STATE.NORMAL,
|
verified: E2EState.Normal,
|
||||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -623,27 +623,27 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
if (!eventSenderTrust) {
|
if (!eventSenderTrust) {
|
||||||
this.setState({
|
this.setState({
|
||||||
verified: E2E_STATE.UNKNOWN,
|
verified: E2EState.Unknown,
|
||||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!eventSenderTrust.isVerified()) {
|
if (!eventSenderTrust.isVerified()) {
|
||||||
this.setState({
|
this.setState({
|
||||||
verified: E2E_STATE.WARNING,
|
verified: E2EState.Warning,
|
||||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!encryptionInfo.authenticated) {
|
if (!encryptionInfo.authenticated) {
|
||||||
this.setState({
|
this.setState({
|
||||||
verified: E2E_STATE.UNAUTHENTICATED,
|
verified: E2EState.Unauthenticated,
|
||||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
verified: E2E_STATE.VERIFIED,
|
verified: E2EState.Verified,
|
||||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -850,13 +850,13 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// event is encrypted, display padlock corresponding to whether or not it is verified
|
// event is encrypted, display padlock corresponding to whether or not it is verified
|
||||||
if (ev.isEncrypted()) {
|
if (ev.isEncrypted()) {
|
||||||
if (this.state.verified === E2E_STATE.NORMAL) {
|
if (this.state.verified === E2EState.Normal) {
|
||||||
return; // no icon if we've not even cross-signed the user
|
return; // no icon if we've not even cross-signed the user
|
||||||
} else if (this.state.verified === E2E_STATE.VERIFIED) {
|
} else if (this.state.verified === E2EState.Verified) {
|
||||||
return; // no icon for verified
|
return; // no icon for verified
|
||||||
} else if (this.state.verified === E2E_STATE.UNAUTHENTICATED) {
|
} else if (this.state.verified === E2EState.Unauthenticated) {
|
||||||
return (<E2ePadlockUnauthenticated />);
|
return (<E2ePadlockUnauthenticated />);
|
||||||
} else if (this.state.verified === E2E_STATE.UNKNOWN) {
|
} else if (this.state.verified === E2EState.Unknown) {
|
||||||
return (<E2ePadlockUnknown />);
|
return (<E2ePadlockUnknown />);
|
||||||
} else {
|
} else {
|
||||||
return (<E2ePadlockUnverified />);
|
return (<E2ePadlockUnverified />);
|
||||||
|
@ -961,9 +961,9 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
mx_EventTile_lastInSection: this.props.lastInSection,
|
mx_EventTile_lastInSection: this.props.lastInSection,
|
||||||
mx_EventTile_contextual: this.props.contextual,
|
mx_EventTile_contextual: this.props.contextual,
|
||||||
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
|
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
|
||||||
mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2E_STATE.VERIFIED,
|
mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2EState.Verified,
|
||||||
mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2E_STATE.WARNING,
|
mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2EState.Warning,
|
||||||
mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN,
|
mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2EState.Unknown,
|
||||||
mx_EventTile_bad: isEncryptionFailure,
|
mx_EventTile_bad: isEncryptionFailure,
|
||||||
mx_EventTile_emote: msgtype === 'm.emote',
|
mx_EventTile_emote: msgtype === 'm.emote',
|
||||||
mx_EventTile_noSender: this.props.hideSender,
|
mx_EventTile_noSender: this.props.hideSender,
|
||||||
|
|
|
@ -14,7 +14,7 @@ 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, { ComponentProps, createRef } from 'react';
|
||||||
import { AllHtmlEntities } from 'html-entities';
|
import { AllHtmlEntities } from 'html-entities';
|
||||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client';
|
import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client';
|
||||||
|
@ -36,6 +36,7 @@ interface IProps {
|
||||||
@replaceableComponent("views.rooms.LinkPreviewWidget")
|
@replaceableComponent("views.rooms.LinkPreviewWidget")
|
||||||
export default class LinkPreviewWidget extends React.Component<IProps> {
|
export default class LinkPreviewWidget extends React.Component<IProps> {
|
||||||
private readonly description = createRef<HTMLDivElement>();
|
private readonly description = createRef<HTMLDivElement>();
|
||||||
|
private image = createRef<HTMLImageElement>();
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.description.current) {
|
if (this.description.current) {
|
||||||
|
@ -59,7 +60,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
|
||||||
src = mediaFromMxc(src).srcHttp;
|
src = mediaFromMxc(src).srcHttp;
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = {
|
const params: Omit<ComponentProps<typeof ImageView>, "onFinished"> = {
|
||||||
src: src,
|
src: src,
|
||||||
width: p["og:image:width"],
|
width: p["og:image:width"],
|
||||||
height: p["og:image:height"],
|
height: p["og:image:height"],
|
||||||
|
@ -68,6 +69,17 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
|
||||||
link: this.props.link,
|
link: this.props.link,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.image.current) {
|
||||||
|
const clientRect = this.image.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
params.thumbnailInfo = {
|
||||||
|
width: clientRect.width,
|
||||||
|
height: clientRect.height,
|
||||||
|
positionX: clientRect.x,
|
||||||
|
positionY: clientRect.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -100,7 +112,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
|
||||||
let img;
|
let img;
|
||||||
if (image) {
|
if (image) {
|
||||||
img = <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
|
img = <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
|
||||||
<img style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }} src={image} onClick={this.onImageClick} />
|
<img ref={this.image} style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }} src={image} onClick={this.onImageClick} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -323,7 +323,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||||
const messageCase = this.getMessageCase();
|
const messageCase = this.getMessageCase();
|
||||||
switch (messageCase) {
|
switch (messageCase) {
|
||||||
case MessageCase.Joining: {
|
case MessageCase.Joining: {
|
||||||
title = this.props.oobData.roomType === RoomType.Space ? _t("Joining space …") : _t("Joining room …");
|
title = this.props.oobData?.roomType === RoomType.Space ? _t("Joining space …") : _t("Joining room …");
|
||||||
showSpinner = true;
|
showSpinner = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,8 @@ import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
|
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
|
||||||
|
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
function addReplyToMessageContent(
|
function addReplyToMessageContent(
|
||||||
content: IContent,
|
content: IContent,
|
||||||
replyToEvent: MatrixEvent,
|
replyToEvent: MatrixEvent,
|
||||||
|
@ -341,7 +343,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
description: errText,
|
description: errText,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log("Command success.");
|
logger.log("Command success.");
|
||||||
if (messageContent) return messageContent;
|
if (messageContent) return messageContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,9 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
import GenericElementContextMenu from "../context_menus/GenericElementContextMenu";
|
import GenericElementContextMenu from "../context_menus/GenericElementContextMenu";
|
||||||
|
import { IApp } from "../../../stores/WidgetStore";
|
||||||
|
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
|
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
|
||||||
// We sit in a context menu, so this should be given to the context menu.
|
// We sit in a context menu, so this should be given to the context menu.
|
||||||
|
@ -98,11 +101,11 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
private removeStickerpickerWidgets = async (): Promise<void> => {
|
private removeStickerpickerWidgets = async (): Promise<void> => {
|
||||||
const scalarClient = await this.acquireScalarClient();
|
const scalarClient = await this.acquireScalarClient();
|
||||||
console.log('Removing Stickerpicker widgets');
|
logger.log('Removing Stickerpicker widgets');
|
||||||
if (this.state.widgetId) {
|
if (this.state.widgetId) {
|
||||||
if (scalarClient) {
|
if (scalarClient) {
|
||||||
scalarClient.disableWidgetAssets(WidgetType.STICKERPICKER, this.state.widgetId).then(() => {
|
scalarClient.disableWidgetAssets(WidgetType.STICKERPICKER, this.state.widgetId).then(() => {
|
||||||
console.log('Assets disabled');
|
logger.log('Assets disabled');
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error('Failed to disable assets');
|
console.error('Failed to disable assets');
|
||||||
});
|
});
|
||||||
|
@ -256,12 +259,16 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
stickerpickerWidget.content.name = stickerpickerWidget.content.name || _t("Stickerpack");
|
stickerpickerWidget.content.name = stickerpickerWidget.content.name || _t("Stickerpack");
|
||||||
|
|
||||||
// FIXME: could this use the same code as other apps?
|
// FIXME: could this use the same code as other apps?
|
||||||
const stickerApp = {
|
const stickerApp: IApp = {
|
||||||
id: stickerpickerWidget.id,
|
id: stickerpickerWidget.id,
|
||||||
url: stickerpickerWidget.content.url,
|
url: stickerpickerWidget.content.url,
|
||||||
name: stickerpickerWidget.content.name,
|
name: stickerpickerWidget.content.name,
|
||||||
type: stickerpickerWidget.content.type,
|
type: stickerpickerWidget.content.type,
|
||||||
data: stickerpickerWidget.content.data,
|
data: stickerpickerWidget.content.data,
|
||||||
|
roomId: stickerpickerWidget.content.roomId,
|
||||||
|
eventId: stickerpickerWidget.content.eventId,
|
||||||
|
avatar_url: stickerpickerWidget.content.avatar_url,
|
||||||
|
creatorUserId: stickerpickerWidget.content.creatorUserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
stickersContent = (
|
stickersContent = (
|
||||||
|
@ -287,9 +294,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
onEditClick={this.launchManageIntegrations}
|
onEditClick={this.launchManageIntegrations}
|
||||||
onDeleteClick={this.removeStickerpickerWidgets}
|
onDeleteClick={this.removeStickerpickerWidgets}
|
||||||
showTitle={false}
|
showTitle={false}
|
||||||
showCancel={false}
|
|
||||||
showPopout={false}
|
showPopout={false}
|
||||||
onMinimiseClick={this.onHideStickersClick}
|
|
||||||
handleMinimisePointerEvents={true}
|
handleMinimisePointerEvents={true}
|
||||||
userWidget={true}
|
userWidget={true}
|
||||||
/>
|
/>
|
||||||
|
@ -345,16 +350,6 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Trigger hiding of the sticker picker overlay
|
|
||||||
* @param {Event} ev Event that triggered the function call
|
|
||||||
*/
|
|
||||||
private onHideStickersClick = (ev: React.MouseEvent): void => {
|
|
||||||
if (this.props.showStickers) {
|
|
||||||
this.props.setShowStickers(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the window is resized
|
* Called when the window is resized
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -17,78 +17,81 @@ limitations under the License.
|
||||||
|
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import Spinner from '../elements/Spinner';
|
import Spinner from '../elements/Spinner';
|
||||||
import withValidation from '../elements/Validation';
|
import withValidation, { IFieldState, IValidationResult } from '../elements/Validation';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import * as sdk from "../../../index";
|
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import PassphraseField from "../auth/PassphraseField";
|
import PassphraseField from "../auth/PassphraseField";
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { PASSWORD_MIN_SCORE } from '../auth/RegistrationForm';
|
import { PASSWORD_MIN_SCORE } from '../auth/RegistrationForm';
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import SetEmailDialog from "../dialogs/SetEmailDialog";
|
||||||
|
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
|
|
||||||
const FIELD_OLD_PASSWORD = 'field_old_password';
|
const FIELD_OLD_PASSWORD = 'field_old_password';
|
||||||
const FIELD_NEW_PASSWORD = 'field_new_password';
|
const FIELD_NEW_PASSWORD = 'field_new_password';
|
||||||
const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
|
const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
|
||||||
|
|
||||||
@replaceableComponent("views.settings.ChangePassword")
|
enum Phase {
|
||||||
export default class ChangePassword extends React.Component {
|
Edit = "edit",
|
||||||
static propTypes = {
|
Uploading = "uploading",
|
||||||
onFinished: PropTypes.func,
|
Error = "error",
|
||||||
onError: PropTypes.func,
|
}
|
||||||
onCheckPassword: PropTypes.func,
|
|
||||||
rowClassName: PropTypes.string,
|
interface IProps {
|
||||||
buttonClassName: PropTypes.string,
|
onFinished?: ({ didSetEmail: boolean }?) => void;
|
||||||
buttonKind: PropTypes.string,
|
onError?: (error: {error: string}) => void;
|
||||||
buttonLabel: PropTypes.string,
|
rowClassName?: string;
|
||||||
confirm: PropTypes.bool,
|
buttonClassName?: string;
|
||||||
|
buttonKind?: string;
|
||||||
|
buttonLabel?: string;
|
||||||
|
confirm?: boolean;
|
||||||
// Whether to autoFocus the new password input
|
// Whether to autoFocus the new password input
|
||||||
autoFocusNewPasswordInput: PropTypes.bool,
|
autoFocusNewPasswordInput?: boolean;
|
||||||
};
|
className?: string;
|
||||||
|
shouldAskForEmail?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
static Phases = {
|
interface IState {
|
||||||
Edit: "edit",
|
fieldValid: {};
|
||||||
Uploading: "uploading",
|
phase: Phase;
|
||||||
Error: "error",
|
oldPassword: string;
|
||||||
};
|
newPassword: string;
|
||||||
|
newPasswordConfirm: string;
|
||||||
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
@replaceableComponent("views.settings.ChangePassword")
|
||||||
|
export default class ChangePassword extends React.Component<IProps, IState> {
|
||||||
|
public static defaultProps: Partial<IProps> = {
|
||||||
onFinished() {},
|
onFinished() {},
|
||||||
onError() {},
|
onError() {},
|
||||||
onCheckPassword(oldPass, newPass, confirmPass) {
|
|
||||||
if (newPass !== confirmPass) {
|
|
||||||
return {
|
|
||||||
error: _t("New passwords don't match"),
|
|
||||||
};
|
|
||||||
} else if (!newPass || newPass.length === 0) {
|
|
||||||
return {
|
|
||||||
error: _t("Passwords can't be empty"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirm: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
confirm: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
fieldValid: {},
|
fieldValid: {},
|
||||||
phase: ChangePassword.Phases.Edit,
|
phase: Phase.Edit,
|
||||||
oldPassword: "",
|
oldPassword: "",
|
||||||
newPassword: "",
|
newPassword: "",
|
||||||
newPasswordConfirm: "",
|
newPasswordConfirm: "",
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
changePassword(oldPassword, newPassword) {
|
private onChangePassword(oldPassword: string, newPassword: string): void {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
if (!this.props.confirm) {
|
if (!this.props.confirm) {
|
||||||
this._changePassword(cli, oldPassword, newPassword);
|
this.changePassword(cli, oldPassword, newPassword);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
|
||||||
Modal.createTrackedDialog('Change Password', '', QuestionDialog, {
|
Modal.createTrackedDialog('Change Password', '', QuestionDialog, {
|
||||||
title: _t("Warning!"),
|
title: _t("Warning!"),
|
||||||
description:
|
description:
|
||||||
|
@ -109,20 +112,20 @@ export default class ChangePassword extends React.Component {
|
||||||
<button
|
<button
|
||||||
key="exportRoomKeys"
|
key="exportRoomKeys"
|
||||||
className="mx_Dialog_primary"
|
className="mx_Dialog_primary"
|
||||||
onClick={this._onExportE2eKeysClicked}
|
onClick={this.onExportE2eKeysClicked}
|
||||||
>
|
>
|
||||||
{ _t('Export E2E room keys') }
|
{ _t('Export E2E room keys') }
|
||||||
</button>,
|
</button>,
|
||||||
],
|
],
|
||||||
onFinished: (confirmed) => {
|
onFinished: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this._changePassword(cli, oldPassword, newPassword);
|
this.changePassword(cli, oldPassword, newPassword);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_changePassword(cli, oldPassword, newPassword) {
|
private changePassword(cli: MatrixClient, oldPassword: string, newPassword: string): void {
|
||||||
const authDict = {
|
const authDict = {
|
||||||
type: 'm.login.password',
|
type: 'm.login.password',
|
||||||
identifier: {
|
identifier: {
|
||||||
|
@ -136,12 +139,12 @@ export default class ChangePassword extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: ChangePassword.Phases.Uploading,
|
phase: Phase.Uploading,
|
||||||
});
|
});
|
||||||
|
|
||||||
cli.setPassword(authDict, newPassword).then(() => {
|
cli.setPassword(authDict, newPassword).then(() => {
|
||||||
if (this.props.shouldAskForEmail) {
|
if (this.props.shouldAskForEmail) {
|
||||||
return this._optionallySetEmail().then((confirmed) => {
|
return this.optionallySetEmail().then((confirmed) => {
|
||||||
this.props.onFinished({
|
this.props.onFinished({
|
||||||
didSetEmail: confirmed,
|
didSetEmail: confirmed,
|
||||||
});
|
});
|
||||||
|
@ -153,7 +156,7 @@ export default class ChangePassword extends React.Component {
|
||||||
this.props.onError(err);
|
this.props.onError(err);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: ChangePassword.Phases.Edit,
|
phase: Phase.Edit,
|
||||||
oldPassword: "",
|
oldPassword: "",
|
||||||
newPassword: "",
|
newPassword: "",
|
||||||
newPasswordConfirm: "",
|
newPasswordConfirm: "",
|
||||||
|
@ -161,16 +164,27 @@ export default class ChangePassword extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_optionallySetEmail() {
|
private checkPassword(oldPass: string, newPass: string, confirmPass: string): {error: string} {
|
||||||
|
if (newPass !== confirmPass) {
|
||||||
|
return {
|
||||||
|
error: _t("New passwords don't match"),
|
||||||
|
};
|
||||||
|
} else if (!newPass || newPass.length === 0) {
|
||||||
|
return {
|
||||||
|
error: _t("Passwords can't be empty"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private optionallySetEmail(): Promise<boolean> {
|
||||||
// Ask for an email otherwise the user has no way to reset their password
|
// Ask for an email otherwise the user has no way to reset their password
|
||||||
const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog");
|
|
||||||
const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, {
|
const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, {
|
||||||
title: _t('Do you want to set an email address?'),
|
title: _t('Do you want to set an email address?'),
|
||||||
});
|
});
|
||||||
return modal.finished.then(([confirmed]) => confirmed);
|
return modal.finished.then(([confirmed]) => confirmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onExportE2eKeysClicked = () => {
|
private onExportE2eKeysClicked = (): void => {
|
||||||
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password',
|
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password',
|
||||||
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||||
{
|
{
|
||||||
|
@ -179,7 +193,7 @@ export default class ChangePassword extends React.Component {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
markFieldValid(fieldID, valid) {
|
private markFieldValid(fieldID: string, valid: boolean): void {
|
||||||
const { fieldValid } = this.state;
|
const { fieldValid } = this.state;
|
||||||
fieldValid[fieldID] = valid;
|
fieldValid[fieldID] = valid;
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -187,19 +201,19 @@ export default class ChangePassword extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeOldPassword = (ev) => {
|
private onChangeOldPassword = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
oldPassword: ev.target.value,
|
oldPassword: ev.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onOldPasswordValidate = async fieldState => {
|
private onOldPasswordValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||||
const result = await this.validateOldPasswordRules(fieldState);
|
const result = await this.validateOldPasswordRules(fieldState);
|
||||||
this.markFieldValid(FIELD_OLD_PASSWORD, result.valid);
|
this.markFieldValid(FIELD_OLD_PASSWORD, result.valid);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
validateOldPasswordRules = withValidation({
|
private validateOldPasswordRules = withValidation({
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
key: "required",
|
key: "required",
|
||||||
|
@ -209,29 +223,29 @@ export default class ChangePassword extends React.Component {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
onChangeNewPassword = (ev) => {
|
private onChangeNewPassword = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
newPassword: ev.target.value,
|
newPassword: ev.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onNewPasswordValidate = result => {
|
private onNewPasswordValidate = (result: IValidationResult): void => {
|
||||||
this.markFieldValid(FIELD_NEW_PASSWORD, result.valid);
|
this.markFieldValid(FIELD_NEW_PASSWORD, result.valid);
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeNewPasswordConfirm = (ev) => {
|
private onChangeNewPasswordConfirm = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
newPasswordConfirm: ev.target.value,
|
newPasswordConfirm: ev.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onNewPasswordConfirmValidate = async fieldState => {
|
private onNewPasswordConfirmValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||||
const result = await this.validatePasswordConfirmRules(fieldState);
|
const result = await this.validatePasswordConfirmRules(fieldState);
|
||||||
this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result.valid);
|
this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result.valid);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
validatePasswordConfirmRules = withValidation({
|
private validatePasswordConfirmRules = withValidation<this>({
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
key: "required",
|
key: "required",
|
||||||
|
@ -248,7 +262,7 @@ export default class ChangePassword extends React.Component {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
onClickChange = async (ev) => {
|
private onClickChange = async (ev: React.MouseEvent | React.FormEvent): Promise<void> => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
|
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
|
||||||
|
@ -260,20 +274,20 @@ export default class ChangePassword extends React.Component {
|
||||||
const oldPassword = this.state.oldPassword;
|
const oldPassword = this.state.oldPassword;
|
||||||
const newPassword = this.state.newPassword;
|
const newPassword = this.state.newPassword;
|
||||||
const confirmPassword = this.state.newPasswordConfirm;
|
const confirmPassword = this.state.newPasswordConfirm;
|
||||||
const err = this.props.onCheckPassword(
|
const err = this.checkPassword(
|
||||||
oldPassword, newPassword, confirmPassword,
|
oldPassword, newPassword, confirmPassword,
|
||||||
);
|
);
|
||||||
if (err) {
|
if (err) {
|
||||||
this.props.onError(err);
|
this.props.onError(err);
|
||||||
} else {
|
} else {
|
||||||
this.changePassword(oldPassword, newPassword);
|
this.onChangePassword(oldPassword, newPassword);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async verifyFieldsBeforeSubmit() {
|
private async verifyFieldsBeforeSubmit(): Promise<boolean> {
|
||||||
// Blur the active element if any, so we first run its blur validation,
|
// Blur the active element if any, so we first run its blur validation,
|
||||||
// which is less strict than the pass we're about to do below for all fields.
|
// which is less strict than the pass we're about to do below for all fields.
|
||||||
const activeElement = document.activeElement;
|
const activeElement = document.activeElement as HTMLElement;
|
||||||
if (activeElement) {
|
if (activeElement) {
|
||||||
activeElement.blur();
|
activeElement.blur();
|
||||||
}
|
}
|
||||||
|
@ -300,7 +314,7 @@ export default class ChangePassword extends React.Component {
|
||||||
|
|
||||||
// Validation and state updates are async, so we need to wait for them to complete
|
// Validation and state updates are async, so we need to wait for them to complete
|
||||||
// first. Queue a `setState` callback and wait for it to resolve.
|
// first. Queue a `setState` callback and wait for it to resolve.
|
||||||
await new Promise(resolve => this.setState({}, resolve));
|
await new Promise<void>((resolve) => this.setState({}, resolve));
|
||||||
|
|
||||||
if (this.allFieldsValid()) {
|
if (this.allFieldsValid()) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -319,7 +333,7 @@ export default class ChangePassword extends React.Component {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
allFieldsValid() {
|
private allFieldsValid(): boolean {
|
||||||
const keys = Object.keys(this.state.fieldValid);
|
const keys = Object.keys(this.state.fieldValid);
|
||||||
for (let i = 0; i < keys.length; ++i) {
|
for (let i = 0; i < keys.length; ++i) {
|
||||||
if (!this.state.fieldValid[keys[i]]) {
|
if (!this.state.fieldValid[keys[i]]) {
|
||||||
|
@ -329,7 +343,7 @@ export default class ChangePassword extends React.Component {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
findFirstInvalidField(fieldIDs) {
|
private findFirstInvalidField(fieldIDs: string[]): Field {
|
||||||
for (const fieldID of fieldIDs) {
|
for (const fieldID of fieldIDs) {
|
||||||
if (!this.state.fieldValid[fieldID] && this[fieldID]) {
|
if (!this.state.fieldValid[fieldID] && this[fieldID]) {
|
||||||
return this[fieldID];
|
return this[fieldID];
|
||||||
|
@ -338,12 +352,12 @@ export default class ChangePassword extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const rowClassName = this.props.rowClassName;
|
const rowClassName = this.props.rowClassName;
|
||||||
const buttonClassName = this.props.buttonClassName;
|
const buttonClassName = this.props.buttonClassName;
|
||||||
|
|
||||||
switch (this.state.phase) {
|
switch (this.state.phase) {
|
||||||
case ChangePassword.Phases.Edit:
|
case Phase.Edit:
|
||||||
return (
|
return (
|
||||||
<form className={this.props.className} onSubmit={this.onClickChange}>
|
<form className={this.props.className} onSubmit={this.onClickChange}>
|
||||||
<div className={rowClassName}>
|
<div className={rowClassName}>
|
||||||
|
@ -385,7 +399,7 @@ export default class ChangePassword extends React.Component {
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
case ChangePassword.Phases.Uploading:
|
case Phase.Uploading:
|
||||||
return (
|
return (
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<Spinner />
|
<Spinner />
|
|
@ -97,9 +97,9 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
|
||||||
const secretStorage = cli.crypto.secretStorage;
|
const secretStorage = cli.crypto.secretStorage;
|
||||||
const crossSigningPublicKeysOnDevice = Boolean(crossSigning.getId());
|
const crossSigningPublicKeysOnDevice = Boolean(crossSigning.getId());
|
||||||
const crossSigningPrivateKeysInStorage = Boolean(await crossSigning.isStoredInSecretStorage(secretStorage));
|
const crossSigningPrivateKeysInStorage = Boolean(await crossSigning.isStoredInSecretStorage(secretStorage));
|
||||||
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
|
const masterPrivateKeyCached = !!(pkCache && (await pkCache.getCrossSigningKeyCache("master")));
|
||||||
const selfSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("self_signing"));
|
const selfSigningPrivateKeyCached = !!(pkCache && (await pkCache.getCrossSigningKeyCache("self_signing")));
|
||||||
const userSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("user_signing"));
|
const userSigningPrivateKeyCached = !!(pkCache && (await pkCache.getCrossSigningKeyCache("user_signing")));
|
||||||
const homeserverSupportsCrossSigning =
|
const homeserverSupportsCrossSigning =
|
||||||
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
|
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
|
||||||
const crossSigningReady = await cli.isCrossSigningReady();
|
const crossSigningReady = await cli.isCrossSigningReady();
|
||||||
|
|
|
@ -262,7 +262,7 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
|
||||||
}
|
}
|
||||||
|
|
||||||
if (beforeJoinRule === joinRule && !restrictedAllowRoomIds) return;
|
if (beforeJoinRule === joinRule && !restrictedAllowRoomIds) return;
|
||||||
if (beforeChange && !await beforeChange(joinRule)) return;
|
if (beforeChange && !(await beforeChange(joinRule))) return;
|
||||||
|
|
||||||
const newContent: IJoinRuleEventContent = {
|
const newContent: IJoinRuleEventContent = {
|
||||||
join_rule: joinRule,
|
join_rule: joinRule,
|
||||||
|
|
|
@ -27,6 +27,8 @@ import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import AvatarSetting from './AvatarSetting';
|
import AvatarSetting from './AvatarSetting';
|
||||||
|
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
userId?: string;
|
userId?: string;
|
||||||
originalDisplayName?: string;
|
originalDisplayName?: string;
|
||||||
|
@ -104,7 +106,7 @@ export default class ProfileSettings extends React.Component<{}, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.avatarFile) {
|
if (this.state.avatarFile) {
|
||||||
console.log(
|
logger.log(
|
||||||
`Uploading new avatar, ${this.state.avatarFile.name} of type ${this.state.avatarFile.type},` +
|
`Uploading new avatar, ${this.state.avatarFile.name} of type ${this.state.avatarFile.type},` +
|
||||||
` (${this.state.avatarFile.size}) bytes`);
|
` (${this.state.avatarFile.size}) bytes`);
|
||||||
const uri = await client.uploadContent(this.state.avatarFile);
|
const uri = await client.uploadContent(this.state.avatarFile);
|
||||||
|
@ -116,7 +118,7 @@ export default class ProfileSettings extends React.Component<{}, IState> {
|
||||||
await client.setAvatarUrl(""); // use empty string as Synapse 500s on undefined
|
await client.setAvatarUrl(""); // use empty string as Synapse 500s on undefined
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Failed to save profile", err);
|
logger.log("Failed to save profile", err);
|
||||||
Modal.createTrackedDialog('Failed to save profile', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to save profile', '', ErrorDialog, {
|
||||||
title: _t("Failed to save your profile"),
|
title: _t("Failed to save your profile"),
|
||||||
description: ((err && err.message) ? err.message : _t("The operation could not be completed")),
|
description: ((err && err.message) ? err.message : _t("The operation could not be completed")),
|
||||||
|
|
|
@ -27,13 +27,31 @@ import QuestionDialog from '../dialogs/QuestionDialog';
|
||||||
import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog';
|
import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog';
|
||||||
import { accessSecretStorage } from '../../../SecurityManager';
|
import { accessSecretStorage } from '../../../SecurityManager';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||||
|
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
loading: boolean;
|
||||||
|
error: null;
|
||||||
|
backupKeyStored: boolean;
|
||||||
|
backupKeyCached: boolean;
|
||||||
|
backupKeyWellFormed: boolean;
|
||||||
|
secretStorageKeyInAccount: boolean;
|
||||||
|
secretStorageReady: boolean;
|
||||||
|
backupInfo: IKeyBackupInfo;
|
||||||
|
backupSigStatus: TrustInfo;
|
||||||
|
sessionsRemaining: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
@replaceableComponent("views.settings.SecureBackupPanel")
|
@replaceableComponent("views.settings.SecureBackupPanel")
|
||||||
export default class SecureBackupPanel extends React.PureComponent {
|
export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||||
constructor(props) {
|
private unmounted = false;
|
||||||
|
|
||||||
|
constructor(props: {}) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this._unmounted = false;
|
|
||||||
this.state = {
|
this.state = {
|
||||||
loading: true,
|
loading: true,
|
||||||
error: null,
|
error: null,
|
||||||
|
@ -48,42 +66,42 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this._checkKeyBackupStatus();
|
this.checkKeyBackupStatus();
|
||||||
|
|
||||||
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatus);
|
MatrixClientPeg.get().on('crypto.keyBackupStatus', this.onKeyBackupStatus);
|
||||||
MatrixClientPeg.get().on(
|
MatrixClientPeg.get().on(
|
||||||
'crypto.keyBackupSessionsRemaining',
|
'crypto.keyBackupSessionsRemaining',
|
||||||
this._onKeyBackupSessionsRemaining,
|
this.onKeyBackupSessionsRemaining,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
this._unmounted = true;
|
this.unmounted = true;
|
||||||
|
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatus);
|
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this.onKeyBackupStatus);
|
||||||
MatrixClientPeg.get().removeListener(
|
MatrixClientPeg.get().removeListener(
|
||||||
'crypto.keyBackupSessionsRemaining',
|
'crypto.keyBackupSessionsRemaining',
|
||||||
this._onKeyBackupSessionsRemaining,
|
this.onKeyBackupSessionsRemaining,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onKeyBackupSessionsRemaining = (sessionsRemaining) => {
|
private onKeyBackupSessionsRemaining = (sessionsRemaining: number): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
sessionsRemaining,
|
sessionsRemaining,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
_onKeyBackupStatus = () => {
|
private onKeyBackupStatus = (): void => {
|
||||||
// This just loads the current backup status rather than forcing
|
// This just loads the current backup status rather than forcing
|
||||||
// a re-check otherwise we risk causing infinite loops
|
// a re-check otherwise we risk causing infinite loops
|
||||||
this._loadBackupStatus();
|
this.loadBackupStatus();
|
||||||
}
|
};
|
||||||
|
|
||||||
async _checkKeyBackupStatus() {
|
private async checkKeyBackupStatus(): Promise<void> {
|
||||||
this._getUpdatedDiagnostics();
|
this.getUpdatedDiagnostics();
|
||||||
try {
|
try {
|
||||||
const { backupInfo, trustInfo } = await MatrixClientPeg.get().checkKeyBackup();
|
const { backupInfo, trustInfo } = await MatrixClientPeg.get().checkKeyBackup();
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -93,8 +111,8 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
backupSigStatus: trustInfo,
|
backupSigStatus: trustInfo,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Unable to fetch check backup status", e);
|
logger.log("Unable to fetch check backup status", e);
|
||||||
if (this._unmounted) return;
|
if (this.unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: e,
|
error: e,
|
||||||
|
@ -104,13 +122,13 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _loadBackupStatus() {
|
private async loadBackupStatus(): Promise<void> {
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
this._getUpdatedDiagnostics();
|
this.getUpdatedDiagnostics();
|
||||||
try {
|
try {
|
||||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||||
const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
|
const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
|
||||||
if (this._unmounted) return;
|
if (this.unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
@ -118,8 +136,8 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
backupSigStatus,
|
backupSigStatus,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Unable to fetch key backup status", e);
|
logger.log("Unable to fetch key backup status", e);
|
||||||
if (this._unmounted) return;
|
if (this.unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: e,
|
error: e,
|
||||||
|
@ -129,7 +147,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _getUpdatedDiagnostics() {
|
private async getUpdatedDiagnostics(): Promise<void> {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const secretStorage = cli.crypto.secretStorage;
|
const secretStorage = cli.crypto.secretStorage;
|
||||||
|
|
||||||
|
@ -140,7 +158,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
||||||
const secretStorageReady = await cli.isSecretStorageReady();
|
const secretStorageReady = await cli.isSecretStorageReady();
|
||||||
|
|
||||||
if (this._unmounted) return;
|
if (this.unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
backupKeyStored,
|
backupKeyStored,
|
||||||
backupKeyCached,
|
backupKeyCached,
|
||||||
|
@ -150,18 +168,18 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_startNewBackup = () => {
|
private startNewBackup = (): void => {
|
||||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||||
import('../../../async-components/views/dialogs/security/CreateKeyBackupDialog'),
|
import('../../../async-components/views/dialogs/security/CreateKeyBackupDialog'),
|
||||||
{
|
{
|
||||||
onFinished: () => {
|
onFinished: () => {
|
||||||
this._loadBackupStatus();
|
this.loadBackupStatus();
|
||||||
},
|
},
|
||||||
}, null, /* priority = */ false, /* static = */ true,
|
}, null, /* priority = */ false, /* static = */ true,
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
_deleteBackup = () => {
|
private deleteBackup = (): void => {
|
||||||
Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, {
|
Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, {
|
||||||
title: _t('Delete Backup'),
|
title: _t('Delete Backup'),
|
||||||
description: _t(
|
description: _t(
|
||||||
|
@ -174,33 +192,33 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
if (!proceed) return;
|
if (!proceed) return;
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
MatrixClientPeg.get().deleteKeyBackupVersion(this.state.backupInfo.version).then(() => {
|
MatrixClientPeg.get().deleteKeyBackupVersion(this.state.backupInfo.version).then(() => {
|
||||||
this._loadBackupStatus();
|
this.loadBackupStatus();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
_restoreBackup = async () => {
|
private restoreBackup = async (): Promise<void> => {
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
||||||
/* priority = */ false, /* static = */ true,
|
/* priority = */ false, /* static = */ true,
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
_resetSecretStorage = async () => {
|
private resetSecretStorage = async (): Promise<void> => {
|
||||||
this.setState({ error: null });
|
this.setState({ error: null });
|
||||||
try {
|
try {
|
||||||
await accessSecretStorage(() => { }, /* forceReset = */ true);
|
await accessSecretStorage(async () => { }, /* forceReset = */ true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error resetting secret storage", e);
|
console.error("Error resetting secret storage", e);
|
||||||
if (this._unmounted) return;
|
if (this.unmounted) return;
|
||||||
this.setState({ error: e });
|
this.setState({ error: e });
|
||||||
}
|
}
|
||||||
if (this._unmounted) return;
|
if (this.unmounted) return;
|
||||||
this._loadBackupStatus();
|
this.loadBackupStatus();
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
|
@ -261,7 +279,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let backupSigStatuses = backupSigStatus.sigs.map((sig, i) => {
|
let backupSigStatuses: React.ReactNode = backupSigStatus.sigs.map((sig, i) => {
|
||||||
const deviceName = sig.device ? (sig.device.getDisplayName() || sig.device.deviceId) : null;
|
const deviceName = sig.device ? (sig.device.getDisplayName() || sig.device.deviceId) : null;
|
||||||
const validity = sub =>
|
const validity = sub =>
|
||||||
<span className={sig.valid ? 'mx_SecureBackupPanel_sigValid' : 'mx_SecureBackupPanel_sigInvalid'}>
|
<span className={sig.valid ? 'mx_SecureBackupPanel_sigValid' : 'mx_SecureBackupPanel_sigInvalid'}>
|
||||||
|
@ -369,14 +387,14 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
</>;
|
</>;
|
||||||
|
|
||||||
actions.push(
|
actions.push(
|
||||||
<AccessibleButton key="restore" kind="primary" onClick={this._restoreBackup}>
|
<AccessibleButton key="restore" kind="primary" onClick={this.restoreBackup}>
|
||||||
{ restoreButtonCaption }
|
{ restoreButtonCaption }
|
||||||
</AccessibleButton>,
|
</AccessibleButton>,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isSecureBackupRequired()) {
|
if (!isSecureBackupRequired()) {
|
||||||
actions.push(
|
actions.push(
|
||||||
<AccessibleButton key="delete" kind="danger" onClick={this._deleteBackup}>
|
<AccessibleButton key="delete" kind="danger" onClick={this.deleteBackup}>
|
||||||
{ _t("Delete Backup") }
|
{ _t("Delete Backup") }
|
||||||
</AccessibleButton>,
|
</AccessibleButton>,
|
||||||
);
|
);
|
||||||
|
@ -390,7 +408,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
<p>{ _t("Back up your keys before signing out to avoid losing them.") }</p>
|
<p>{ _t("Back up your keys before signing out to avoid losing them.") }</p>
|
||||||
</>;
|
</>;
|
||||||
actions.push(
|
actions.push(
|
||||||
<AccessibleButton key="setup" kind="primary" onClick={this._startNewBackup}>
|
<AccessibleButton key="setup" kind="primary" onClick={this.startNewBackup}>
|
||||||
{ _t("Set up") }
|
{ _t("Set up") }
|
||||||
</AccessibleButton>,
|
</AccessibleButton>,
|
||||||
);
|
);
|
||||||
|
@ -398,7 +416,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
|
|
||||||
if (secretStorageKeyInAccount) {
|
if (secretStorageKeyInAccount) {
|
||||||
actions.push(
|
actions.push(
|
||||||
<AccessibleButton key="reset" kind="danger" onClick={this._resetSecretStorage}>
|
<AccessibleButton key="reset" kind="danger" onClick={this.resetSecretStorage}>
|
||||||
{ _t("Reset") }
|
{ _t("Reset") }
|
||||||
</AccessibleButton>,
|
</AccessibleButton>,
|
||||||
);
|
);
|
|
@ -16,16 +16,16 @@ 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 { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||||
import Field from "../../elements/Field";
|
import Field from "../../elements/Field";
|
||||||
import AccessibleButton from "../../elements/AccessibleButton";
|
import AccessibleButton from "../../elements/AccessibleButton";
|
||||||
import * as Email from "../../../../email";
|
import * as Email from "../../../../email";
|
||||||
import AddThreepid from "../../../../AddThreepid";
|
import AddThreepid from "../../../../AddThreepid";
|
||||||
import * as sdk from '../../../../index';
|
|
||||||
import Modal from '../../../../Modal';
|
import Modal from '../../../../Modal';
|
||||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||||
|
import ErrorDialog from "../../dialogs/ErrorDialog";
|
||||||
|
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
TODO: Improve the UX for everything in here.
|
TODO: Improve the UX for everything in here.
|
||||||
|
@ -39,42 +39,45 @@ places to communicate errors - these should be replaced with inline validation w
|
||||||
that is available.
|
that is available.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class ExistingEmailAddress extends React.Component {
|
interface IExistingEmailAddressProps {
|
||||||
static propTypes = {
|
email: IThreepid;
|
||||||
email: PropTypes.object.isRequired,
|
onRemoved: (emails: IThreepid) => void;
|
||||||
onRemoved: PropTypes.func.isRequired,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
interface IExistingEmailAddressState {
|
||||||
super();
|
verifyRemove: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExistingEmailAddress extends React.Component<IExistingEmailAddressProps, IExistingEmailAddressState> {
|
||||||
|
constructor(props: IExistingEmailAddressProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
verifyRemove: false,
|
verifyRemove: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRemove = (e) => {
|
private onRemove = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.setState({ verifyRemove: true });
|
this.setState({ verifyRemove: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
_onDontRemove = (e) => {
|
private onDontRemove = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.setState({ verifyRemove: false });
|
this.setState({ verifyRemove: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
_onActuallyRemove = (e) => {
|
private onActuallyRemove = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
MatrixClientPeg.get().deleteThreePid(this.props.email.medium, this.props.email.address).then(() => {
|
MatrixClientPeg.get().deleteThreePid(this.props.email.medium, this.props.email.address).then(() => {
|
||||||
return this.props.onRemoved(this.props.email);
|
return this.props.onRemoved(this.props.email);
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
console.error("Unable to remove contact information: " + err);
|
console.error("Unable to remove contact information: " + err);
|
||||||
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
|
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
|
||||||
title: _t("Unable to remove contact information"),
|
title: _t("Unable to remove contact information"),
|
||||||
|
@ -83,7 +86,7 @@ export class ExistingEmailAddress extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
if (this.state.verifyRemove) {
|
if (this.state.verifyRemove) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_ExistingEmailAddress">
|
<div className="mx_ExistingEmailAddress">
|
||||||
|
@ -91,14 +94,14 @@ export class ExistingEmailAddress extends React.Component {
|
||||||
{ _t("Remove %(email)s?", { email: this.props.email.address } ) }
|
{ _t("Remove %(email)s?", { email: this.props.email.address } ) }
|
||||||
</span>
|
</span>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this._onActuallyRemove}
|
onClick={this.onActuallyRemove}
|
||||||
kind="danger_sm"
|
kind="danger_sm"
|
||||||
className="mx_ExistingEmailAddress_confirmBtn"
|
className="mx_ExistingEmailAddress_confirmBtn"
|
||||||
>
|
>
|
||||||
{ _t("Remove") }
|
{ _t("Remove") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this._onDontRemove}
|
onClick={this.onDontRemove}
|
||||||
kind="link_sm"
|
kind="link_sm"
|
||||||
className="mx_ExistingEmailAddress_confirmBtn"
|
className="mx_ExistingEmailAddress_confirmBtn"
|
||||||
>
|
>
|
||||||
|
@ -111,7 +114,7 @@ export class ExistingEmailAddress extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className="mx_ExistingEmailAddress">
|
<div className="mx_ExistingEmailAddress">
|
||||||
<span className="mx_ExistingEmailAddress_email">{ this.props.email.address }</span>
|
<span className="mx_ExistingEmailAddress_email">{ this.props.email.address }</span>
|
||||||
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
|
<AccessibleButton onClick={this.onRemove} kind="danger_sm">
|
||||||
{ _t("Remove") }
|
{ _t("Remove") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -119,14 +122,21 @@ export class ExistingEmailAddress extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.settings.account.EmailAddresses")
|
interface IProps {
|
||||||
export default class EmailAddresses extends React.Component {
|
emails: IThreepid[];
|
||||||
static propTypes = {
|
onEmailsChange: (emails: Partial<IThreepid>[]) => void;
|
||||||
emails: PropTypes.array.isRequired,
|
|
||||||
onEmailsChange: PropTypes.func.isRequired,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
interface IState {
|
||||||
|
verifying: boolean;
|
||||||
|
addTask: any; // FIXME: When AddThreepid is TSfied
|
||||||
|
continueDisabled: boolean;
|
||||||
|
newEmailAddress: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.settings.account.EmailAddresses")
|
||||||
|
export default class EmailAddresses extends React.Component<IProps, IState> {
|
||||||
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -137,24 +147,23 @@ export default class EmailAddresses extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRemoved = (address) => {
|
private onRemoved = (address): void => {
|
||||||
const emails = this.props.emails.filter((e) => e !== address);
|
const emails = this.props.emails.filter((e) => e !== address);
|
||||||
this.props.onEmailsChange(emails);
|
this.props.onEmailsChange(emails);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onChangeNewEmailAddress = (e) => {
|
private onChangeNewEmailAddress = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
newEmailAddress: e.target.value,
|
newEmailAddress: e.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onAddClick = (e) => {
|
private onAddClick = (e: React.FormEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!this.state.newEmailAddress) return;
|
if (!this.state.newEmailAddress) return;
|
||||||
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
const email = this.state.newEmailAddress;
|
const email = this.state.newEmailAddress;
|
||||||
|
|
||||||
// TODO: Inline field validation
|
// TODO: Inline field validation
|
||||||
|
@ -181,7 +190,7 @@ export default class EmailAddresses extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onContinueClick = (e) => {
|
private onContinueClick = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -192,7 +201,7 @@ export default class EmailAddresses extends React.Component {
|
||||||
const email = this.state.newEmailAddress;
|
const email = this.state.newEmailAddress;
|
||||||
const emails = [
|
const emails = [
|
||||||
...this.props.emails,
|
...this.props.emails,
|
||||||
{ address: email, medium: "email" },
|
{ address: email, medium: ThreepidMedium.Email },
|
||||||
];
|
];
|
||||||
this.props.onEmailsChange(emails);
|
this.props.onEmailsChange(emails);
|
||||||
newEmailAddress = "";
|
newEmailAddress = "";
|
||||||
|
@ -205,7 +214,6 @@ export default class EmailAddresses extends React.Component {
|
||||||
});
|
});
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
this.setState({ continueDisabled: false });
|
this.setState({ continueDisabled: false });
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
|
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
|
||||||
Modal.createTrackedDialog("Email hasn't been verified yet", "", ErrorDialog, {
|
Modal.createTrackedDialog("Email hasn't been verified yet", "", ErrorDialog, {
|
||||||
title: _t("Your email address hasn't been verified yet"),
|
title: _t("Your email address hasn't been verified yet"),
|
||||||
|
@ -222,13 +230,13 @@ export default class EmailAddresses extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const existingEmailElements = this.props.emails.map((e) => {
|
const existingEmailElements = this.props.emails.map((e) => {
|
||||||
return <ExistingEmailAddress email={e} onRemoved={this._onRemoved} key={e.address} />;
|
return <ExistingEmailAddress email={e} onRemoved={this.onRemoved} key={e.address} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
let addButton = (
|
let addButton = (
|
||||||
<AccessibleButton onClick={this._onAddClick} kind="primary">
|
<AccessibleButton onClick={this.onAddClick} kind="primary">
|
||||||
{ _t("Add") }
|
{ _t("Add") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
@ -237,7 +245,7 @@ export default class EmailAddresses extends React.Component {
|
||||||
<div>
|
<div>
|
||||||
<div>{ _t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.") }</div>
|
<div>{ _t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.") }</div>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this._onContinueClick}
|
onClick={this.onContinueClick}
|
||||||
kind="primary"
|
kind="primary"
|
||||||
disabled={this.state.continueDisabled}
|
disabled={this.state.continueDisabled}
|
||||||
>
|
>
|
||||||
|
@ -251,7 +259,7 @@ export default class EmailAddresses extends React.Component {
|
||||||
<div className="mx_EmailAddresses">
|
<div className="mx_EmailAddresses">
|
||||||
{ existingEmailElements }
|
{ existingEmailElements }
|
||||||
<form
|
<form
|
||||||
onSubmit={this._onAddClick}
|
onSubmit={this.onAddClick}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
noValidate={true}
|
noValidate={true}
|
||||||
className="mx_EmailAddresses_new"
|
className="mx_EmailAddresses_new"
|
||||||
|
@ -262,7 +270,7 @@ export default class EmailAddresses extends React.Component {
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
disabled={this.state.verifying}
|
disabled={this.state.verifying}
|
||||||
value={this.state.newEmailAddress}
|
value={this.state.newEmailAddress}
|
||||||
onChange={this._onChangeNewEmailAddress}
|
onChange={this.onChangeNewEmailAddress}
|
||||||
/>
|
/>
|
||||||
{ addButton }
|
{ addButton }
|
||||||
</form>
|
</form>
|
|
@ -16,16 +16,17 @@ 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 { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||||
import Field from "../../elements/Field";
|
import Field from "../../elements/Field";
|
||||||
import AccessibleButton from "../../elements/AccessibleButton";
|
import AccessibleButton from "../../elements/AccessibleButton";
|
||||||
import AddThreepid from "../../../../AddThreepid";
|
import AddThreepid from "../../../../AddThreepid";
|
||||||
import CountryDropdown from "../../auth/CountryDropdown";
|
import CountryDropdown from "../../auth/CountryDropdown";
|
||||||
import * as sdk from '../../../../index';
|
|
||||||
import Modal from '../../../../Modal';
|
import Modal from '../../../../Modal';
|
||||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||||
|
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
||||||
|
import ErrorDialog from "../../dialogs/ErrorDialog";
|
||||||
|
import { PhoneNumberCountryDefinition } from "../../../../phonenumber";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
TODO: Improve the UX for everything in here.
|
TODO: Improve the UX for everything in here.
|
||||||
|
@ -34,42 +35,45 @@ This is a copy/paste of EmailAddresses, mostly.
|
||||||
|
|
||||||
// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic
|
// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic
|
||||||
|
|
||||||
export class ExistingPhoneNumber extends React.Component {
|
interface IExistingPhoneNumberProps {
|
||||||
static propTypes = {
|
msisdn: IThreepid;
|
||||||
msisdn: PropTypes.object.isRequired,
|
onRemoved: (phoneNumber: IThreepid) => void;
|
||||||
onRemoved: PropTypes.func.isRequired,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
interface IExistingPhoneNumberState {
|
||||||
super();
|
verifyRemove: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExistingPhoneNumber extends React.Component<IExistingPhoneNumberProps, IExistingPhoneNumberState> {
|
||||||
|
constructor(props: IExistingPhoneNumberProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
verifyRemove: false,
|
verifyRemove: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRemove = (e) => {
|
private onRemove = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.setState({ verifyRemove: true });
|
this.setState({ verifyRemove: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
_onDontRemove = (e) => {
|
private onDontRemove = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.setState({ verifyRemove: false });
|
this.setState({ verifyRemove: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
_onActuallyRemove = (e) => {
|
private onActuallyRemove = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
MatrixClientPeg.get().deleteThreePid(this.props.msisdn.medium, this.props.msisdn.address).then(() => {
|
MatrixClientPeg.get().deleteThreePid(this.props.msisdn.medium, this.props.msisdn.address).then(() => {
|
||||||
return this.props.onRemoved(this.props.msisdn);
|
return this.props.onRemoved(this.props.msisdn);
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
console.error("Unable to remove contact information: " + err);
|
console.error("Unable to remove contact information: " + err);
|
||||||
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
|
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
|
||||||
title: _t("Unable to remove contact information"),
|
title: _t("Unable to remove contact information"),
|
||||||
|
@ -78,7 +82,7 @@ export class ExistingPhoneNumber extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
if (this.state.verifyRemove) {
|
if (this.state.verifyRemove) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_ExistingPhoneNumber">
|
<div className="mx_ExistingPhoneNumber">
|
||||||
|
@ -86,14 +90,14 @@ export class ExistingPhoneNumber extends React.Component {
|
||||||
{ _t("Remove %(phone)s?", { phone: this.props.msisdn.address }) }
|
{ _t("Remove %(phone)s?", { phone: this.props.msisdn.address }) }
|
||||||
</span>
|
</span>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this._onActuallyRemove}
|
onClick={this.onActuallyRemove}
|
||||||
kind="danger_sm"
|
kind="danger_sm"
|
||||||
className="mx_ExistingPhoneNumber_confirmBtn"
|
className="mx_ExistingPhoneNumber_confirmBtn"
|
||||||
>
|
>
|
||||||
{ _t("Remove") }
|
{ _t("Remove") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this._onDontRemove}
|
onClick={this.onDontRemove}
|
||||||
kind="link_sm"
|
kind="link_sm"
|
||||||
className="mx_ExistingPhoneNumber_confirmBtn"
|
className="mx_ExistingPhoneNumber_confirmBtn"
|
||||||
>
|
>
|
||||||
|
@ -106,7 +110,7 @@ export class ExistingPhoneNumber extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className="mx_ExistingPhoneNumber">
|
<div className="mx_ExistingPhoneNumber">
|
||||||
<span className="mx_ExistingPhoneNumber_address">+{ this.props.msisdn.address }</span>
|
<span className="mx_ExistingPhoneNumber_address">+{ this.props.msisdn.address }</span>
|
||||||
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
|
<AccessibleButton onClick={this.onRemove} kind="danger_sm">
|
||||||
{ _t("Remove") }
|
{ _t("Remove") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -114,19 +118,30 @@ export class ExistingPhoneNumber extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.settings.account.PhoneNumbers")
|
interface IProps {
|
||||||
export default class PhoneNumbers extends React.Component {
|
msisdns: IThreepid[];
|
||||||
static propTypes = {
|
onMsisdnsChange: (phoneNumbers: Partial<IThreepid>[]) => void;
|
||||||
msisdns: PropTypes.array.isRequired,
|
|
||||||
onMsisdnsChange: PropTypes.func.isRequired,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
interface IState {
|
||||||
|
verifying: boolean;
|
||||||
|
verifyError: string;
|
||||||
|
verifyMsisdn: string;
|
||||||
|
addTask: any; // FIXME: When AddThreepid is TSfied
|
||||||
|
continueDisabled: boolean;
|
||||||
|
phoneCountry: string;
|
||||||
|
newPhoneNumber: string;
|
||||||
|
newPhoneNumberCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.settings.account.PhoneNumbers")
|
||||||
|
export default class PhoneNumbers extends React.Component<IProps, IState> {
|
||||||
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
verifying: false,
|
verifying: false,
|
||||||
verifyError: false,
|
verifyError: null,
|
||||||
verifyMsisdn: "",
|
verifyMsisdn: "",
|
||||||
addTask: null,
|
addTask: null,
|
||||||
continueDisabled: false,
|
continueDisabled: false,
|
||||||
|
@ -136,30 +151,29 @@ export default class PhoneNumbers extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRemoved = (address) => {
|
private onRemoved = (address: IThreepid): void => {
|
||||||
const msisdns = this.props.msisdns.filter((e) => e !== address);
|
const msisdns = this.props.msisdns.filter((e) => e !== address);
|
||||||
this.props.onMsisdnsChange(msisdns);
|
this.props.onMsisdnsChange(msisdns);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onChangeNewPhoneNumber = (e) => {
|
private onChangeNewPhoneNumber = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
newPhoneNumber: e.target.value,
|
newPhoneNumber: e.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onChangeNewPhoneNumberCode = (e) => {
|
private onChangeNewPhoneNumberCode = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
newPhoneNumberCode: e.target.value,
|
newPhoneNumberCode: e.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onAddClick = (e) => {
|
private onAddClick = (e: React.MouseEvent | React.FormEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!this.state.newPhoneNumber) return;
|
if (!this.state.newPhoneNumber) return;
|
||||||
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
const phoneNumber = this.state.newPhoneNumber;
|
const phoneNumber = this.state.newPhoneNumber;
|
||||||
const phoneCountry = this.state.phoneCountry;
|
const phoneCountry = this.state.phoneCountry;
|
||||||
|
|
||||||
|
@ -178,7 +192,7 @@ export default class PhoneNumbers extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onContinueClick = (e) => {
|
private onContinueClick = (e: React.MouseEvent | React.FormEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -190,7 +204,7 @@ export default class PhoneNumbers extends React.Component {
|
||||||
if (finished) {
|
if (finished) {
|
||||||
const msisdns = [
|
const msisdns = [
|
||||||
...this.props.msisdns,
|
...this.props.msisdns,
|
||||||
{ address, medium: "msisdn" },
|
{ address, medium: ThreepidMedium.Phone },
|
||||||
];
|
];
|
||||||
this.props.onMsisdnsChange(msisdns);
|
this.props.onMsisdnsChange(msisdns);
|
||||||
newPhoneNumber = "";
|
newPhoneNumber = "";
|
||||||
|
@ -207,7 +221,6 @@ export default class PhoneNumbers extends React.Component {
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
this.setState({ continueDisabled: false });
|
this.setState({ continueDisabled: false });
|
||||||
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
|
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
console.error("Unable to verify phone number: " + err);
|
console.error("Unable to verify phone number: " + err);
|
||||||
Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, {
|
Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, {
|
||||||
title: _t("Unable to verify phone number."),
|
title: _t("Unable to verify phone number."),
|
||||||
|
@ -219,17 +232,17 @@ export default class PhoneNumbers extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onCountryChanged = (e) => {
|
private onCountryChanged = (country: PhoneNumberCountryDefinition): void => {
|
||||||
this.setState({ phoneCountry: e.iso2 });
|
this.setState({ phoneCountry: country.iso2 });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const existingPhoneElements = this.props.msisdns.map((p) => {
|
const existingPhoneElements = this.props.msisdns.map((p) => {
|
||||||
return <ExistingPhoneNumber msisdn={p} onRemoved={this._onRemoved} key={p.address} />;
|
return <ExistingPhoneNumber msisdn={p} onRemoved={this.onRemoved} key={p.address} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
let addVerifySection = (
|
let addVerifySection = (
|
||||||
<AccessibleButton onClick={this._onAddClick} kind="primary">
|
<AccessibleButton onClick={this.onAddClick} kind="primary">
|
||||||
{ _t("Add") }
|
{ _t("Add") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
@ -243,17 +256,17 @@ export default class PhoneNumbers extends React.Component {
|
||||||
<br />
|
<br />
|
||||||
{ this.state.verifyError }
|
{ this.state.verifyError }
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={this._onContinueClick} autoComplete="off" noValidate={true}>
|
<form onSubmit={this.onContinueClick} autoComplete="off" noValidate={true}>
|
||||||
<Field
|
<Field
|
||||||
type="text"
|
type="text"
|
||||||
label={_t("Verification code")}
|
label={_t("Verification code")}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
disabled={this.state.continueDisabled}
|
disabled={this.state.continueDisabled}
|
||||||
value={this.state.newPhoneNumberCode}
|
value={this.state.newPhoneNumberCode}
|
||||||
onChange={this._onChangeNewPhoneNumberCode}
|
onChange={this.onChangeNewPhoneNumberCode}
|
||||||
/>
|
/>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this._onContinueClick}
|
onClick={this.onContinueClick}
|
||||||
kind="primary"
|
kind="primary"
|
||||||
disabled={this.state.continueDisabled}
|
disabled={this.state.continueDisabled}
|
||||||
>
|
>
|
||||||
|
@ -264,7 +277,7 @@ export default class PhoneNumbers extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const phoneCountry = <CountryDropdown onOptionChange={this._onCountryChanged}
|
const phoneCountry = <CountryDropdown onOptionChange={this.onCountryChanged}
|
||||||
className="mx_PhoneNumbers_country"
|
className="mx_PhoneNumbers_country"
|
||||||
value={this.state.phoneCountry}
|
value={this.state.phoneCountry}
|
||||||
disabled={this.state.verifying}
|
disabled={this.state.verifying}
|
||||||
|
@ -275,7 +288,7 @@ export default class PhoneNumbers extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className="mx_PhoneNumbers">
|
<div className="mx_PhoneNumbers">
|
||||||
{ existingPhoneElements }
|
{ existingPhoneElements }
|
||||||
<form onSubmit={this._onAddClick} autoComplete="off" noValidate={true} className="mx_PhoneNumbers_new">
|
<form onSubmit={this.onAddClick} autoComplete="off" noValidate={true} className="mx_PhoneNumbers_new">
|
||||||
<div className="mx_PhoneNumbers_input">
|
<div className="mx_PhoneNumbers_input">
|
||||||
<Field
|
<Field
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -284,7 +297,7 @@ export default class PhoneNumbers extends React.Component {
|
||||||
disabled={this.state.verifying}
|
disabled={this.state.verifying}
|
||||||
prefixComponent={phoneCountry}
|
prefixComponent={phoneCountry}
|
||||||
value={this.state.newPhoneNumber}
|
value={this.state.newPhoneNumber}
|
||||||
onChange={this._onChangeNewPhoneNumber}
|
onChange={this.onChangeNewPhoneNumber}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
|
@ -16,14 +16,15 @@ 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 { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||||
import * as sdk from '../../../../index';
|
|
||||||
import Modal from '../../../../Modal';
|
import Modal from '../../../../Modal';
|
||||||
import AddThreepid from '../../../../AddThreepid';
|
import AddThreepid from '../../../../AddThreepid';
|
||||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||||
|
import { IThreepid } from "matrix-js-sdk/src/@types/threepids";
|
||||||
|
import ErrorDialog from "../../dialogs/ErrorDialog";
|
||||||
|
import AccessibleButton from "../../elements/AccessibleButton";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
TODO: Improve the UX for everything in here.
|
TODO: Improve the UX for everything in here.
|
||||||
|
@ -41,12 +42,19 @@ that is available.
|
||||||
TODO: Reduce all the copying between account vs. discovery components.
|
TODO: Reduce all the copying between account vs. discovery components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class EmailAddress extends React.Component {
|
interface IEmailAddressProps {
|
||||||
static propTypes = {
|
email: IThreepid;
|
||||||
email: PropTypes.object.isRequired,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
interface IEmailAddressState {
|
||||||
|
verifying: boolean;
|
||||||
|
addTask: any; // FIXME: When AddThreepid is TSfied
|
||||||
|
continueDisabled: boolean;
|
||||||
|
bound: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EmailAddress extends React.Component<IEmailAddressProps, IEmailAddressState> {
|
||||||
|
constructor(props: IEmailAddressProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const { bound } = props.email;
|
const { bound } = props.email;
|
||||||
|
@ -60,17 +68,17 @@ export class EmailAddress extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, camelcase
|
||||||
|
public UNSAFE_componentWillReceiveProps(nextProps: IEmailAddressProps): void {
|
||||||
const { bound } = nextProps.email;
|
const { bound } = nextProps.email;
|
||||||
this.setState({ bound });
|
this.setState({ bound });
|
||||||
}
|
}
|
||||||
|
|
||||||
async changeBinding({ bind, label, errorTitle }) {
|
private async changeBinding({ bind, label, errorTitle }): Promise<void> {
|
||||||
if (!await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
if (!await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||||
return this.changeBindingTangledAddBind({ bind, label, errorTitle });
|
return this.changeBindingTangledAddBind({ bind, label, errorTitle });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
const { medium, address } = this.props.email;
|
const { medium, address } = this.props.email;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -103,8 +111,7 @@ export class EmailAddress extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async changeBindingTangledAddBind({ bind, label, errorTitle }) {
|
private async changeBindingTangledAddBind({ bind, label, errorTitle }): Promise<void> {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
const { medium, address } = this.props.email;
|
const { medium, address } = this.props.email;
|
||||||
|
|
||||||
const task = new AddThreepid();
|
const task = new AddThreepid();
|
||||||
|
@ -139,7 +146,7 @@ export class EmailAddress extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onRevokeClick = (e) => {
|
private onRevokeClick = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.changeBinding({
|
this.changeBinding({
|
||||||
|
@ -147,9 +154,9 @@ export class EmailAddress extends React.Component {
|
||||||
label: "revoke",
|
label: "revoke",
|
||||||
errorTitle: _t("Unable to revoke sharing for email address"),
|
errorTitle: _t("Unable to revoke sharing for email address"),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onShareClick = (e) => {
|
private onShareClick = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.changeBinding({
|
this.changeBinding({
|
||||||
|
@ -157,9 +164,9 @@ export class EmailAddress extends React.Component {
|
||||||
label: "share",
|
label: "share",
|
||||||
errorTitle: _t("Unable to share email address"),
|
errorTitle: _t("Unable to share email address"),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onContinueClick = async (e) => {
|
private onContinueClick = async (e: React.MouseEvent): Promise<void> => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -173,7 +180,6 @@ export class EmailAddress extends React.Component {
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ continueDisabled: false });
|
this.setState({ continueDisabled: false });
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
|
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
|
||||||
Modal.createTrackedDialog("E-mail hasn't been verified yet", "", ErrorDialog, {
|
Modal.createTrackedDialog("E-mail hasn't been verified yet", "", ErrorDialog, {
|
||||||
title: _t("Your email address hasn't been verified yet"),
|
title: _t("Your email address hasn't been verified yet"),
|
||||||
|
@ -188,10 +194,9 @@ export class EmailAddress extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
const { address } = this.props.email;
|
const { address } = this.props.email;
|
||||||
const { verifying, bound } = this.state;
|
const { verifying, bound } = this.state;
|
||||||
|
|
||||||
|
@ -234,14 +239,13 @@ export class EmailAddress extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
interface IProps {
|
||||||
@replaceableComponent("views.settings.discovery.EmailAddresses")
|
emails: IThreepid[];
|
||||||
export default class EmailAddresses extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
emails: PropTypes.array.isRequired,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
@replaceableComponent("views.settings.discovery.EmailAddresses")
|
||||||
|
export default class EmailAddresses extends React.Component<IProps> {
|
||||||
|
public render(): JSX.Element {
|
||||||
let content;
|
let content;
|
||||||
if (this.props.emails.length > 0) {
|
if (this.props.emails.length > 0) {
|
||||||
content = this.props.emails.map((e) => {
|
content = this.props.emails.map((e) => {
|
|
@ -16,14 +16,16 @@ 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 { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||||
import * as sdk from '../../../../index';
|
|
||||||
import Modal from '../../../../Modal';
|
import Modal from '../../../../Modal';
|
||||||
import AddThreepid from '../../../../AddThreepid';
|
import AddThreepid from '../../../../AddThreepid';
|
||||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||||
|
import { IThreepid } from "matrix-js-sdk/src/@types/threepids";
|
||||||
|
import ErrorDialog from "../../dialogs/ErrorDialog";
|
||||||
|
import Field from "../../elements/Field";
|
||||||
|
import AccessibleButton from "../../elements/AccessibleButton";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
TODO: Improve the UX for everything in here.
|
TODO: Improve the UX for everything in here.
|
||||||
|
@ -32,12 +34,21 @@ This is a copy/paste of EmailAddresses, mostly.
|
||||||
|
|
||||||
// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic
|
// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic
|
||||||
|
|
||||||
export class PhoneNumber extends React.Component {
|
interface IPhoneNumberProps {
|
||||||
static propTypes = {
|
msisdn: IThreepid;
|
||||||
msisdn: PropTypes.object.isRequired,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
interface IPhoneNumberState {
|
||||||
|
verifying: boolean;
|
||||||
|
verificationCode: string;
|
||||||
|
addTask: any; // FIXME: When AddThreepid is TSfied
|
||||||
|
continueDisabled: boolean;
|
||||||
|
bound: boolean;
|
||||||
|
verifyError: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PhoneNumber extends React.Component<IPhoneNumberProps, IPhoneNumberState> {
|
||||||
|
constructor(props: IPhoneNumberProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const { bound } = props.msisdn;
|
const { bound } = props.msisdn;
|
||||||
|
@ -48,21 +59,22 @@ export class PhoneNumber extends React.Component {
|
||||||
addTask: null,
|
addTask: null,
|
||||||
continueDisabled: false,
|
continueDisabled: false,
|
||||||
bound,
|
bound,
|
||||||
|
verifyError: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, camelcase
|
||||||
|
public UNSAFE_componentWillReceiveProps(nextProps: IPhoneNumberProps): void {
|
||||||
const { bound } = nextProps.msisdn;
|
const { bound } = nextProps.msisdn;
|
||||||
this.setState({ bound });
|
this.setState({ bound });
|
||||||
}
|
}
|
||||||
|
|
||||||
async changeBinding({ bind, label, errorTitle }) {
|
private async changeBinding({ bind, label, errorTitle }): Promise<void> {
|
||||||
if (!await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
if (!await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
|
||||||
return this.changeBindingTangledAddBind({ bind, label, errorTitle });
|
return this.changeBindingTangledAddBind({ bind, label, errorTitle });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
const { medium, address } = this.props.msisdn;
|
const { medium, address } = this.props.msisdn;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -99,8 +111,7 @@ export class PhoneNumber extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async changeBindingTangledAddBind({ bind, label, errorTitle }) {
|
private async changeBindingTangledAddBind({ bind, label, errorTitle }): Promise<void> {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
const { medium, address } = this.props.msisdn;
|
const { medium, address } = this.props.msisdn;
|
||||||
|
|
||||||
const task = new AddThreepid();
|
const task = new AddThreepid();
|
||||||
|
@ -139,7 +150,7 @@ export class PhoneNumber extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onRevokeClick = (e) => {
|
private onRevokeClick = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.changeBinding({
|
this.changeBinding({
|
||||||
|
@ -147,9 +158,9 @@ export class PhoneNumber extends React.Component {
|
||||||
label: "revoke",
|
label: "revoke",
|
||||||
errorTitle: _t("Unable to revoke sharing for phone number"),
|
errorTitle: _t("Unable to revoke sharing for phone number"),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onShareClick = (e) => {
|
private onShareClick = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.changeBinding({
|
this.changeBinding({
|
||||||
|
@ -157,15 +168,15 @@ export class PhoneNumber extends React.Component {
|
||||||
label: "share",
|
label: "share",
|
||||||
errorTitle: _t("Unable to share phone number"),
|
errorTitle: _t("Unable to share phone number"),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onVerificationCodeChange = (e) => {
|
private onVerificationCodeChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
verificationCode: e.target.value,
|
verificationCode: e.target.value,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onContinueClick = async (e) => {
|
private onContinueClick = async (e: React.MouseEvent | React.FormEvent): Promise<void> => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -183,7 +194,6 @@ export class PhoneNumber extends React.Component {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ continueDisabled: false });
|
this.setState({ continueDisabled: false });
|
||||||
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
|
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
console.error("Unable to verify phone number: " + err);
|
console.error("Unable to verify phone number: " + err);
|
||||||
Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, {
|
Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, {
|
||||||
title: _t("Unable to verify phone number."),
|
title: _t("Unable to verify phone number."),
|
||||||
|
@ -193,11 +203,9 @@ export class PhoneNumber extends React.Component {
|
||||||
this.setState({ verifyError: _t("Incorrect verification code") });
|
this.setState({ verifyError: _t("Incorrect verification code") });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
const Field = sdk.getComponent('elements.Field');
|
|
||||||
const { address } = this.props.msisdn;
|
const { address } = this.props.msisdn;
|
||||||
const { verifying, bound } = this.state;
|
const { verifying, bound } = this.state;
|
||||||
|
|
||||||
|
@ -247,13 +255,13 @@ export class PhoneNumber extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.settings.discovery.PhoneNumbers")
|
interface IProps {
|
||||||
export default class PhoneNumbers extends React.Component {
|
msisdns: IThreepid[];
|
||||||
static propTypes = {
|
|
||||||
msisdns: PropTypes.array.isRequired,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
@replaceableComponent("views.settings.discovery.PhoneNumbers")
|
||||||
|
export default class PhoneNumbers extends React.Component<IProps> {
|
||||||
|
public render(): JSX.Element {
|
||||||
let content;
|
let content;
|
||||||
if (this.props.msisdns.length > 0) {
|
if (this.props.msisdns.length > 0) {
|
||||||
content = this.props.msisdns.map((e) => {
|
content = this.props.msisdns.map((e) => {
|
|
@ -15,45 +15,46 @@ 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 RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
|
import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
|
||||||
import * as sdk from "../../../../..";
|
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
import dis from "../../../../../dispatcher/dispatcher";
|
import dis from "../../../../../dispatcher/dispatcher";
|
||||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
|
import UrlPreviewSettings from "../../../room_settings/UrlPreviewSettings";
|
||||||
|
import RelatedGroupSettings from "../../../room_settings/RelatedGroupSettings";
|
||||||
|
import AliasSettings from "../../../room_settings/AliasSettings";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
roomId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
isRoomPublished: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.settings.tabs.room.GeneralRoomSettingsTab")
|
@replaceableComponent("views.settings.tabs.room.GeneralRoomSettingsTab")
|
||||||
export default class GeneralRoomSettingsTab extends React.Component {
|
export default class GeneralRoomSettingsTab extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
public static contextType = MatrixClientContext;
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isRoomPublished: false, // loaded async
|
isRoomPublished: false, // loaded async
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_onLeaveClick = () => {
|
private onLeaveClick = (): void => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'leave_room',
|
action: 'leave_room',
|
||||||
room_id: this.props.roomId,
|
room_id: this.props.roomId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const AliasSettings = sdk.getComponent("room_settings.AliasSettings");
|
|
||||||
const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
|
|
||||||
const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
|
|
||||||
|
|
||||||
const client = this.context;
|
const client = this.context;
|
||||||
const room = client.getRoom(this.props.roomId);
|
const room = client.getRoom(this.props.roomId);
|
||||||
|
|
||||||
|
@ -110,7 +111,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
||||||
|
|
||||||
<span className='mx_SettingsTab_subheading'>{ _t("Leave room") }</span>
|
<span className='mx_SettingsTab_subheading'>{ _t("Leave room") }</span>
|
||||||
<div className='mx_SettingsTab_section'>
|
<div className='mx_SettingsTab_section'>
|
||||||
<AccessibleButton kind='danger' onClick={this._onLeaveClick}>
|
<AccessibleButton kind='danger' onClick={this.onLeaveClick}>
|
||||||
{ _t('Leave room') }
|
{ _t('Leave room') }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
|
@ -24,16 +23,21 @@ import SettingsStore from '../../../../../settings/SettingsStore';
|
||||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
roomId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
currentSound: string;
|
||||||
|
uploadedFile: File;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.settings.tabs.room.NotificationsSettingsTab")
|
@replaceableComponent("views.settings.tabs.room.NotificationsSettingsTab")
|
||||||
export default class NotificationsSettingsTab extends React.Component {
|
export default class NotificationsSettingsTab extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private soundUpload = createRef<HTMLInputElement>();
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
_soundUpload = createRef();
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
currentSound: "default",
|
currentSound: "default",
|
||||||
|
@ -42,7 +46,8 @@ export default class NotificationsSettingsTab extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||||
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||||
|
public UNSAFE_componentWillMount(): void {
|
||||||
const soundData = Notifier.getSoundForRoom(this.props.roomId);
|
const soundData = Notifier.getSoundForRoom(this.props.roomId);
|
||||||
if (!soundData) {
|
if (!soundData) {
|
||||||
return;
|
return;
|
||||||
|
@ -50,14 +55,14 @@ export default class NotificationsSettingsTab extends React.Component {
|
||||||
this.setState({ currentSound: soundData.name || soundData.url });
|
this.setState({ currentSound: soundData.name || soundData.url });
|
||||||
}
|
}
|
||||||
|
|
||||||
async _triggerUploader(e) {
|
private triggerUploader = async (e: React.MouseEvent): Promise<void> => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this._soundUpload.current.click();
|
this.soundUpload.current.click();
|
||||||
}
|
};
|
||||||
|
|
||||||
async _onSoundUploadChanged(e) {
|
private onSoundUploadChanged = (e: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
|
||||||
if (!e.target.files || !e.target.files.length) {
|
if (!e.target.files || !e.target.files.length) {
|
||||||
this.setState({
|
this.setState({
|
||||||
uploadedFile: null,
|
uploadedFile: null,
|
||||||
|
@ -69,23 +74,23 @@ export default class NotificationsSettingsTab extends React.Component {
|
||||||
this.setState({
|
this.setState({
|
||||||
uploadedFile: file,
|
uploadedFile: file,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
async _onClickSaveSound(e) {
|
private onClickSaveSound = async (e: React.MouseEvent): Promise<void> => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this._saveSound();
|
await this.saveSound();
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
console.error(
|
console.error(
|
||||||
`Unable to save notification sound for ${this.props.roomId}`,
|
`Unable to save notification sound for ${this.props.roomId}`,
|
||||||
);
|
);
|
||||||
console.error(ex);
|
console.error(ex);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
async _saveSound() {
|
private async saveSound(): Promise<void> {
|
||||||
if (!this.state.uploadedFile) {
|
if (!this.state.uploadedFile) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -122,7 +127,7 @@ export default class NotificationsSettingsTab extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_clearSound(e) {
|
private clearSound = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
SettingsStore.setValue(
|
SettingsStore.setValue(
|
||||||
|
@ -135,9 +140,9 @@ export default class NotificationsSettingsTab extends React.Component {
|
||||||
this.setState({
|
this.setState({
|
||||||
currentSound: "default",
|
currentSound: "default",
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
let currentUploadedFile = null;
|
let currentUploadedFile = null;
|
||||||
if (this.state.uploadedFile) {
|
if (this.state.uploadedFile) {
|
||||||
currentUploadedFile = (
|
currentUploadedFile = (
|
||||||
|
@ -154,23 +159,23 @@ export default class NotificationsSettingsTab extends React.Component {
|
||||||
<span className='mx_SettingsTab_subheading'>{ _t("Sounds") }</span>
|
<span className='mx_SettingsTab_subheading'>{ _t("Sounds") }</span>
|
||||||
<div>
|
<div>
|
||||||
<span>{ _t("Notification sound") }: <code>{ this.state.currentSound }</code></span><br />
|
<span>{ _t("Notification sound") }: <code>{ this.state.currentSound }</code></span><br />
|
||||||
<AccessibleButton className="mx_NotificationSound_resetSound" disabled={this.state.currentSound == "default"} onClick={this._clearSound.bind(this)} kind="primary">
|
<AccessibleButton className="mx_NotificationSound_resetSound" disabled={this.state.currentSound == "default"} onClick={this.clearSound} kind="primary">
|
||||||
{ _t("Reset") }
|
{ _t("Reset") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3>{ _t("Set a new custom sound") }</h3>
|
<h3>{ _t("Set a new custom sound") }</h3>
|
||||||
<form autoComplete="off" noValidate={true}>
|
<form autoComplete="off" noValidate={true}>
|
||||||
<input ref={this._soundUpload} className="mx_NotificationSound_soundUpload" type="file" onChange={this._onSoundUploadChanged.bind(this)} accept="audio/*" />
|
<input ref={this.soundUpload} className="mx_NotificationSound_soundUpload" type="file" onChange={this.onSoundUploadChanged} accept="audio/*" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{ currentUploadedFile }
|
{ currentUploadedFile }
|
||||||
|
|
||||||
<AccessibleButton className="mx_NotificationSound_browse" onClick={this._triggerUploader.bind(this)} kind="primary">
|
<AccessibleButton className="mx_NotificationSound_browse" onClick={this.triggerUploader} kind="primary">
|
||||||
{ _t("Browse") }
|
{ _t("Browse") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
|
||||||
<AccessibleButton className="mx_NotificationSound_save" disabled={this.state.uploadedFile == null} onClick={this._onClickSaveSound.bind(this)} kind="primary">
|
<AccessibleButton className="mx_NotificationSound_save" disabled={this.state.uploadedFile == null} onClick={this.onClickSaveSound} kind="primary">
|
||||||
{ _t("Save") }
|
{ _t("Save") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<br />
|
<br />
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue