Merge branches 'develop' and 't3chguy/context_menus' of github.com:matrix-org/matrix-react-sdk into t3chguy/context_menus

 Conflicts:
	src/components/views/context_menus/RoomTileContextMenu.js
This commit is contained in:
Michael Telatynski 2019-11-28 16:25:59 +00:00
commit 6d69ec17d9
214 changed files with 6620 additions and 1948 deletions

View file

@ -19,6 +19,7 @@ limitations under the License.
*/
import dis from './dispatcher';
import BaseEventIndexManager from './indexing/BaseEventIndexManager';
/**
* Base class for classes that provide platform-specific functionality
@ -151,4 +152,14 @@ export default class BasePlatform {
async setMinimizeToTrayEnabled(enabled: boolean): void {
throw new Error("Unimplemented");
}
/**
* Get our platform specific EventIndexManager.
*
* @return {BaseEventIndexManager} The EventIndex manager for our platform,
* can be null if the platform doesn't support event indexing.
*/
getEventIndexingManager(): BaseEventIndexManager | null {
return null;
}
}

View file

@ -80,13 +80,26 @@ function play(audioId) {
// which listens?
const audio = document.getElementById(audioId);
if (audio) {
const playAudio = async () => {
try {
// This still causes the chrome debugger to break on promise rejection if
// the promise is rejected, even though we're catching the exception.
await audio.play();
} catch (e) {
// This is usually because the user hasn't interacted with the document,
// or chrome doesn't think so and is denying the request. Not sure what
// we can really do here...
// https://github.com/vector-im/riot-web/issues/7657
console.log("Unable to play audio clip", e);
}
};
if (audioPromises[audioId]) {
audioPromises[audioId] = audioPromises[audioId].then(()=>{
audio.load();
return audio.play();
return playAudio();
});
} else {
audioPromises[audioId] = audio.play();
audioPromises[audioId] = playAudio();
}
}
}
@ -322,7 +335,7 @@ function _onAction(payload) {
});
return;
} else if (members.length === 2) {
console.log("Place %s call in %s", payload.type, payload.room_id);
console.info("Place %s call in %s", payload.type, payload.room_id);
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
placeCall(call);
} else { // > 2
@ -337,7 +350,7 @@ function _onAction(payload) {
}
break;
case 'place_conference_call':
console.log("Place conference call in %s", payload.room_id);
console.info("Place conference call in %s", payload.room_id);
_startCallApp(payload.room_id, payload.type);
break;
case 'incoming_call':
@ -382,7 +395,7 @@ function _onAction(payload) {
}
async function _startCallApp(roomId, type) {
// check for a working integrations manager. Technically we could put
// check for a working integration manager. Technically we could put
// the state event in anyway, but the resulting widget would then not
// work for us. Better that the user knows before everyone else in the
// room sees it.
@ -495,6 +508,17 @@ async function _startCallApp(roomId, type) {
// with the dispatcher once
if (!global.mxCallHandler) {
dis.register(_onAction);
// add empty handlers for media actions, otherwise the media keys
// end up causing the audio elements with our ring/ringback etc
// audio clips in to play.
if (navigator.mediaSession) {
navigator.mediaSession.setActionHandler('play', function() {});
navigator.mediaSession.setActionHandler('pause', function() {});
navigator.mediaSession.setActionHandler('seekbackward', function() {});
navigator.mediaSession.setActionHandler('seekforward', function() {});
navigator.mediaSession.setActionHandler('previoustrack', function() {});
navigator.mediaSession.setActionHandler('nexttrack', function() {});
}
}
const callHandler = {

View file

@ -17,7 +17,6 @@ limitations under the License.
'use strict';
import Promise from 'bluebird';
import extend from './extend';
import dis from './dispatcher';
import MatrixClientPeg from './MatrixClientPeg';
@ -59,40 +58,38 @@ export class UploadCanceledError extends Error {}
* and a thumbnail key.
*/
function createThumbnail(element, inputWidth, inputHeight, mimeType) {
const deferred = Promise.defer();
return new Promise((resolve) => {
let targetWidth = inputWidth;
let targetHeight = inputHeight;
if (targetHeight > MAX_HEIGHT) {
targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
targetHeight = MAX_HEIGHT;
}
if (targetWidth > MAX_WIDTH) {
targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth));
targetWidth = MAX_WIDTH;
}
let targetWidth = inputWidth;
let targetHeight = inputHeight;
if (targetHeight > MAX_HEIGHT) {
targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
targetHeight = MAX_HEIGHT;
}
if (targetWidth > MAX_WIDTH) {
targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth));
targetWidth = MAX_WIDTH;
}
const canvas = document.createElement("canvas");
canvas.width = targetWidth;
canvas.height = targetHeight;
canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight);
canvas.toBlob(function(thumbnail) {
deferred.resolve({
info: {
thumbnail_info: {
w: targetWidth,
h: targetHeight,
mimetype: thumbnail.type,
size: thumbnail.size,
const canvas = document.createElement("canvas");
canvas.width = targetWidth;
canvas.height = targetHeight;
canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight);
canvas.toBlob(function(thumbnail) {
resolve({
info: {
thumbnail_info: {
w: targetWidth,
h: targetHeight,
mimetype: thumbnail.type,
size: thumbnail.size,
},
w: inputWidth,
h: inputHeight,
},
w: inputWidth,
h: inputHeight,
},
thumbnail: thumbnail,
});
}, mimeType);
return deferred.promise;
thumbnail: thumbnail,
});
}, mimeType);
});
}
/**
@ -179,30 +176,29 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
* @return {Promise} A promise that resolves with the video image element.
*/
function loadVideoElement(videoFile) {
const deferred = Promise.defer();
return new Promise((resolve, reject) => {
// Load the file into an html element
const video = document.createElement("video");
// Load the file into an html element
const video = document.createElement("video");
const reader = new FileReader();
const reader = new FileReader();
reader.onload = function(e) {
video.src = e.target.result;
reader.onload = function(e) {
video.src = e.target.result;
// Once ready, returns its size
// Wait until we have enough data to thumbnail the first frame.
video.onloadeddata = function() {
deferred.resolve(video);
// Once ready, returns its size
// Wait until we have enough data to thumbnail the first frame.
video.onloadeddata = function() {
resolve(video);
};
video.onerror = function(e) {
reject(e);
};
};
video.onerror = function(e) {
deferred.reject(e);
reader.onerror = function(e) {
reject(e);
};
};
reader.onerror = function(e) {
deferred.reject(e);
};
reader.readAsDataURL(videoFile);
return deferred.promise;
reader.readAsDataURL(videoFile);
});
}
/**
@ -236,16 +232,16 @@ function infoForVideoFile(matrixClient, roomId, videoFile) {
* is read.
*/
function readFileAsArrayBuffer(file) {
const deferred = Promise.defer();
const reader = new FileReader();
reader.onload = function(e) {
deferred.resolve(e.target.result);
};
reader.onerror = function(e) {
deferred.reject(e);
};
reader.readAsArrayBuffer(file);
return deferred.promise;
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function(e) {
resolve(e.target.result);
};
reader.onerror = function(e) {
reject(e);
};
reader.readAsArrayBuffer(file);
});
}
/**
@ -461,33 +457,34 @@ export default class ContentMessages {
content.info.mimetype = file.type;
}
const def = Promise.defer();
if (file.type.indexOf('image/') == 0) {
content.msgtype = 'm.image';
infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{
extend(content.info, imageInfo);
def.resolve();
}, (error)=>{
console.error(error);
const prom = new Promise((resolve) => {
if (file.type.indexOf('image/') == 0) {
content.msgtype = 'm.image';
infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{
extend(content.info, imageInfo);
resolve();
}, (error)=>{
console.error(error);
content.msgtype = 'm.file';
resolve();
});
} else if (file.type.indexOf('audio/') == 0) {
content.msgtype = 'm.audio';
resolve();
} else if (file.type.indexOf('video/') == 0) {
content.msgtype = 'm.video';
infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{
extend(content.info, videoInfo);
resolve();
}, (error)=>{
content.msgtype = 'm.file';
resolve();
});
} else {
content.msgtype = 'm.file';
def.resolve();
});
} else if (file.type.indexOf('audio/') == 0) {
content.msgtype = 'm.audio';
def.resolve();
} else if (file.type.indexOf('video/') == 0) {
content.msgtype = 'm.video';
infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{
extend(content.info, videoInfo);
def.resolve();
}, (error)=>{
content.msgtype = 'm.file';
def.resolve();
});
} else {
content.msgtype = 'm.file';
def.resolve();
}
resolve();
}
});
const upload = {
fileName: file.name || 'Attachment',
@ -509,7 +506,7 @@ export default class ContentMessages {
dis.dispatch({action: 'upload_progress', upload: upload});
}
return def.promise.then(function() {
return prom.then(function() {
// XXX: upload.promise must be the promise that
// is returned by uploadFile as it has an abort()
// method hacked onto it.

View file

@ -21,6 +21,7 @@ import MultiInviter from './utils/MultiInviter';
import { _t } from './languageHandler';
import MatrixClientPeg from './MatrixClientPeg';
import GroupStore from './stores/GroupStore';
import {allSettled} from "./utils/promise";
export function showGroupInviteDialog(groupId) {
return new Promise((resolve, reject) => {
@ -118,7 +119,7 @@ function _onGroupInviteFinished(groupId, addrs) {
function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
const matrixClient = MatrixClientPeg.get();
const errorList = [];
return Promise.all(addrs.map((addr) => {
return allSettled(addrs.map((addr) => {
return GroupStore
.addRoomToGroup(groupId, addr.address, addRoomsPublicly)
.catch(() => { errorList.push(addr.address); })
@ -138,7 +139,7 @@ function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
groups.push(groupId);
return MatrixClientPeg.get().sendStateEvent(roomId, 'm.room.related_groups', {groups}, '');
}
}).reflect();
});
})).then(() => {
if (errorList.length === 0) {
return;

View file

@ -16,10 +16,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
import Matrix from 'matrix-js-sdk';
import MatrixClientPeg from './MatrixClientPeg';
import EventIndexPeg from './indexing/EventIndexPeg';
import createMatrixClient from './utils/createMatrixClient';
import Analytics from './Analytics';
import Notifier from './Notifier';
@ -36,6 +36,7 @@ import * as StorageManager from './utils/StorageManager';
import SettingsStore from "./settings/SettingsStore";
import TypingStore from "./stores/TypingStore";
import {IntegrationManagers} from "./integrations/IntegrationManagers";
import {Mjolnir} from "./mjolnir/Mjolnir";
/**
* Called at startup, to attempt to build a logged-in Matrix session. It tries
@ -312,18 +313,14 @@ async function _restoreFromLocalStorage(opts) {
function _handleLoadSessionFailure(e) {
console.error("Unable to load session", e);
const def = Promise.defer();
const SessionRestoreErrorDialog =
sdk.getComponent('views.dialogs.SessionRestoreErrorDialog');
Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, {
const modal = Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, {
error: e.message,
onFinished: (success) => {
def.resolve(success);
},
});
return def.promise.then((success) => {
return modal.finished.then(([success]) => {
if (success) {
// user clicked continue.
_clearStorage();
@ -528,7 +525,7 @@ export function logout() {
console.log("Failed to call logout API: token will not be invalidated");
onLoggedOut();
},
).done();
);
}
export function softLogout() {
@ -585,8 +582,14 @@ async function startMatrixClient(startSyncing=true) {
IntegrationManagers.sharedInstance().startWatching();
ActiveWidgetStore.start();
// Start Mjolnir even though we haven't checked the feature flag yet. Starting
// the thing just wastes CPU cycles, but should result in no actual functionality
// being exposed to the user.
Mjolnir.sharedInstance().start();
if (startSyncing) {
await MatrixClientPeg.start();
await EventIndexPeg.init();
} else {
console.warn("Caller requested only auxiliary services be started");
await MatrixClientPeg.assign();
@ -605,20 +608,20 @@ async function startMatrixClient(startSyncing=true) {
* Stops a running client and all related services, and clears persistent
* storage. Used after a session has been logged out.
*/
export function onLoggedOut() {
export async function onLoggedOut() {
_isLoggingOut = false;
// Ensure that we dispatch a view change **before** stopping the client so
// so that React components unmount first. This avoids React soft crashes
// that can occur when components try to use a null client.
dis.dispatch({action: 'on_logged_out'}, true);
stopMatrixClient();
_clearStorage().done();
await _clearStorage();
}
/**
* @returns {Promise} promise which resolves once the stores have been cleared
*/
function _clearStorage() {
async function _clearStorage() {
Analytics.logout();
if (window.localStorage) {
@ -630,7 +633,9 @@ function _clearStorage() {
// we'll never make any requests, so can pass a bogus HS URL
baseUrl: "",
});
return cli.clearStores();
await EventIndexPeg.deleteEventIndex();
await cli.clearStores();
}
/**
@ -645,7 +650,9 @@ export function stopMatrixClient(unsetClient=true) {
Presence.stop();
ActiveWidgetStore.stop();
IntegrationManagers.sharedInstance().stopWatching();
Mjolnir.sharedInstance().stop();
if (DMRoomMap.shared()) DMRoomMap.shared().stop();
EventIndexPeg.stop();
const cli = MatrixClientPeg.get();
if (cli) {
cli.stopClient();
@ -653,6 +660,7 @@ export function stopMatrixClient(unsetClient=true) {
if (unsetClient) {
MatrixClientPeg.unset();
EventIndexPeg.unset();
}
}
}

View file

@ -220,6 +220,16 @@ class MatrixClientPeg {
identityServer: new IdentityAuthClient(),
};
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
// TODO: Cross-signing keys are temporarily in memory only. A
// separate task in the cross-signing project will build from here.
const keys = [];
opts.cryptoCallbacks = {
getCrossSigningKey: k => keys[k],
saveCrossSigningKeys: newKeys => Object.assign(keys, newKeys),
};
}
this.matrixClient = createMatrixClient(opts);
// we're going to add eventlisteners for each matrix event tile, so the

View file

@ -23,7 +23,7 @@ import Analytics from './Analytics';
import sdk from './index';
import dis from './dispatcher';
import { _t } from './languageHandler';
import Promise from "bluebird";
import {defer} from "./utils/promise";
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
@ -202,7 +202,7 @@ class ModalManager {
}
_getCloseFn(modal, props) {
const deferred = Promise.defer();
const deferred = defer();
return [(...args) => {
deferred.resolve(args);
if (props && props.onFinished) props.onFinished.apply(null, args);

View file

@ -146,7 +146,7 @@ const Notifier = {
}
document.body.appendChild(audioElement);
}
audioElement.play();
await audioElement.play();
} catch (ex) {
console.warn("Caught error when trying to fetch room notification sound:", ex);
}
@ -198,7 +198,7 @@ const Notifier = {
if (enable) {
// Attempt to get permission from user
plaf.requestNotificationPermission().done((result) => {
plaf.requestNotificationPermission().then((result) => {
if (result !== 'granted') {
// The permission request was dismissed or denied
// TODO: Support alternative branding in messaging

View file

@ -96,7 +96,7 @@ class Presence {
try {
await MatrixClientPeg.get().setPresence(this.state);
console.log("Presence: %s", newState);
console.info("Presence: %s", newState);
} catch (err) {
console.error("Failed to set presence: %s", err);
this.state = oldState;

View file

@ -35,7 +35,7 @@ module.exports = {
},
resend: function(event) {
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
MatrixClientPeg.get().resendEvent(event, room).done(function(res) {
MatrixClientPeg.get().resendEvent(event, room).then(function(res) {
dis.dispatch({
action: 'message_sent',
event: event,

View file

@ -28,8 +28,8 @@ export function levelRoleMap(usersDefault) {
export function textualPowerLevel(level, usersDefault) {
const LEVEL_ROLE_MAP = levelRoleMap(usersDefault);
if (LEVEL_ROLE_MAP[level]) {
return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${usersDefault})`);
return LEVEL_ROLE_MAP[level];
} else {
return level;
return _t("Custom (%(level)s)", {level});
}
}

35
src/RoomAliasCache.js Normal file
View file

@ -0,0 +1,35 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* This is meant to be a cache of room alias to room ID so that moving between
* rooms happens smoothly (for example using browser back / forward buttons).
*
* For the moment, it's in memory only and so only applies for the current
* session for simplicity, but could be extended further in the future.
*
* A similar thing could also be achieved via `pushState` with a state object,
* but keeping it separate like this seems easier in case we do want to extend.
*/
const aliasToIDMap = new Map();
export function storeRoomAliasInCache(alias, id) {
aliasToIDMap.set(alias, id);
}
export function getCachedRoomIDForAlias(alias) {
return aliasToIDMap.get(alias);
}

View file

@ -203,10 +203,13 @@ function _showAnyInviteErrors(addrs, room, inviter) {
}
if (errorList.length > 0) {
// React 16 doesn't let us use `errorList.join(<br />)` anymore, so this is our solution
const description = <div>{errorList.map(e => <div key={e}>{e}</div>)}</div>;
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, {
title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
description: errorList.join(<br />),
description,
});
}
}
@ -225,4 +228,3 @@ function _getDirectMessageRooms(addr) {
});
return rooms;
}

View file

@ -17,7 +17,6 @@ limitations under the License.
import MatrixClientPeg from './MatrixClientPeg';
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
import Promise from 'bluebird';
export const ALL_MESSAGES_LOUD = 'all_messages_loud';
export const ALL_MESSAGES = 'all_messages';

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import MatrixClientPeg from './MatrixClientPeg';
import Promise from 'bluebird';
/**
* Given a room object, return the alias we should use for it,

View file

@ -16,7 +16,6 @@ limitations under the License.
*/
import url from 'url';
import Promise from 'bluebird';
import SettingsStore from "./settings/SettingsStore";
import { Service, startTermsFlow, TermsNotSignedError } from './Terms';
const request = require('browser-request');

View file

@ -279,7 +279,7 @@ function inviteUser(event, roomId, userId) {
}
}
client.invite(roomId, userId).done(function() {
client.invite(roomId, userId).then(function() {
sendResponse(event, {
success: true,
});
@ -398,7 +398,7 @@ function setPlumbingState(event, roomId, status) {
sendError(event, _t('You need to be logged in.'));
return;
}
client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).done(() => {
client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).then(() => {
sendResponse(event, {
success: true,
});
@ -414,7 +414,7 @@ function setBotOptions(event, roomId, userId) {
sendError(event, _t('You need to be logged in.'));
return;
}
client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => {
client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).then(() => {
sendResponse(event, {
success: true,
});
@ -444,7 +444,7 @@ function setBotPower(event, roomId, userId, level) {
},
);
client.setPowerLevel(roomId, userId, level, powerEvent).done(() => {
client.setPowerLevel(roomId, userId, level, powerEvent).then(() => {
sendResponse(event, {
success: true,
});

138
src/Searching.js Normal file
View file

@ -0,0 +1,138 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import EventIndexPeg from "./indexing/EventIndexPeg";
import MatrixClientPeg from "./MatrixClientPeg";
function serverSideSearch(term, roomId = undefined) {
let filter;
if (roomId !== undefined) {
// XXX: it's unintuitive that the filter for searching doesn't have
// the same shape as the v2 filter API :(
filter = {
rooms: [roomId],
};
}
const searchPromise = MatrixClientPeg.get().searchRoomEvents({
filter,
term,
});
return searchPromise;
}
async function combinedSearch(searchTerm) {
// Create two promises, one for the local search, one for the
// server-side search.
const serverSidePromise = serverSideSearch(searchTerm);
const localPromise = localSearch(searchTerm);
// Wait for both promises to resolve.
await Promise.all([serverSidePromise, localPromise]);
// Get both search results.
const localResult = await localPromise;
const serverSideResult = await serverSidePromise;
// Combine the search results into one result.
const result = {};
// Our localResult and serverSideResult are both ordered by
// recency separately, when we combine them the order might not
// be the right one so we need to sort them.
const compare = (a, b) => {
const aEvent = a.context.getEvent().event;
const bEvent = b.context.getEvent().event;
if (aEvent.origin_server_ts >
bEvent.origin_server_ts) return -1;
if (aEvent.origin_server_ts <
bEvent.origin_server_ts) return 1;
return 0;
};
result.count = localResult.count + serverSideResult.count;
result.results = localResult.results.concat(
serverSideResult.results).sort(compare);
result.highlights = localResult.highlights.concat(
serverSideResult.highlights);
return result;
}
async function localSearch(searchTerm, roomId = undefined) {
const searchArgs = {
search_term: searchTerm,
before_limit: 1,
after_limit: 1,
order_by_recency: true,
room_id: undefined,
};
if (roomId !== undefined) {
searchArgs.room_id = roomId;
}
const eventIndex = EventIndexPeg.get();
const localResult = await eventIndex.search(searchArgs);
const response = {
search_categories: {
room_events: localResult,
},
};
const emptyResult = {
results: [],
highlights: [],
};
const result = MatrixClientPeg.get()._processRoomEventsSearch(
emptyResult, response);
return result;
}
function eventIndexSearch(term, roomId = undefined) {
let searchPromise;
if (roomId !== undefined) {
if (MatrixClientPeg.get().isRoomEncrypted(roomId)) {
// The search is for a single encrypted room, use our local
// search method.
searchPromise = localSearch(term, roomId);
} else {
// The search is for a single non-encrypted room, use the
// server-side search.
searchPromise = serverSideSearch(term, roomId);
}
} else {
// Search across all rooms, combine a server side search and a
// local search.
searchPromise = combinedSearch(term);
}
return searchPromise;
}
export default function eventSearch(term, roomId = undefined) {
const eventIndex = EventIndexPeg.get();
if (eventIndex === null) return serverSideSearch(term, roomId);
else return eventIndexSearch(term, roomId);
}

View file

@ -28,7 +28,6 @@ import { linkifyAndSanitizeHtml } from './HtmlUtils';
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
import WidgetUtils from "./utils/WidgetUtils";
import {textToHtmlRainbow} from "./utils/colour";
import Promise from "bluebird";
import { getAddressType } from './UserAddress';
import { abbreviateUrl } from './utils/UrlUtils';
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils';

View file

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
import classNames from 'classnames';
import MatrixClientPeg from './MatrixClientPeg';

View file

@ -358,13 +358,25 @@ function textForCallHangupEvent(event) {
function textForCallInviteEvent(event) {
const senderName = event.sender ? event.sender.name : _t('Someone');
// FIXME: Find a better way to determine this from the event?
let callType = "voice";
let isVoice = true;
if (event.getContent().offer && event.getContent().offer.sdp &&
event.getContent().offer.sdp.indexOf('m=video') !== -1) {
callType = "video";
isVoice = false;
}
const isSupported = MatrixClientPeg.get().supportsVoip();
// This ladder could be reduced down to a couple string variables, however other languages
// can have a hard time translating those strings. In an effort to make translations easier
// and more accurate, we break out the string-based variables to a couple booleans.
if (isVoice && isSupported) {
return _t("%(senderName)s placed a voice call.", {senderName});
} else if (isVoice && !isSupported) {
return _t("%(senderName)s placed a voice call. (not supported by this browser)", {senderName});
} else if (!isVoice && isSupported) {
return _t("%(senderName)s placed a video call.", {senderName});
} else if (!isVoice && !isSupported) {
return _t("%(senderName)s placed a video call. (not supported by this browser)", {senderName});
}
const supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
return _t('%(senderName)s placed a %(callType)s call.', {senderName, callType}) + ' ' + supported;
}
function textForThreePidInviteEvent(event) {

View file

@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from "bluebird";
// const OUTBOUND_API_NAME = 'toWidget';
// Initiate requests using the "toWidget" postMessage API and handle responses

View file

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
import {createNewMatrixCall, Room} from "matrix-js-sdk";
import CallHandler from './CallHandler';
import MatrixClientPeg from "./MatrixClientPeg";

View file

@ -26,7 +26,7 @@ import RoomProvider from './RoomProvider';
import UserProvider from './UserProvider';
import EmojiProvider from './EmojiProvider';
import NotifProvider from './NotifProvider';
import Promise from 'bluebird';
import {timeout} from "../utils/promise";
export type SelectionRange = {
beginning: boolean, // whether the selection is in the first block of the editor or not
@ -77,23 +77,16 @@ export default class Autocompleter {
while the user is interacting with the list, which makes it difficult
to predict whether an action will actually do what is intended
*/
const completionsList = await Promise.all(
// Array of inspections of promises that might timeout. Instead of allowing a
// single timeout to reject the Promise.all, reflect each one and once they've all
// settled, filter for the fulfilled ones
this.providers.map(provider =>
provider
.getCompletions(query, selection, force)
.timeout(PROVIDER_COMPLETION_TIMEOUT)
.reflect(),
),
);
const completionsList = await Promise.all(this.providers.map(provider => {
return timeout(provider.getCompletions(query, selection, force), null, PROVIDER_COMPLETION_TIMEOUT);
}));
// map then filter to maintain the index for the map-operation, for this.providers to line up
return completionsList.map((completions, i) => {
if (!completions || !completions.length) return;
return completionsList.filter(
(inspection) => inspection.isFulfilled(),
).map((completionsState, i) => {
return {
completions: completionsState.value(),
completions,
provider: this.providers[i],
/* the currently matched "command" the completer tried to complete
@ -102,6 +95,6 @@ export default class Autocompleter {
*/
command: this.providers[i].getCurrentCommand(query, selection, force),
};
});
}).filter(Boolean);
}
}

View file

@ -19,7 +19,6 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import Promise from 'bluebird';
import MatrixClientPeg from '../../MatrixClientPeg';
import sdk from '../../index';
import dis from '../../dispatcher';
@ -38,6 +37,7 @@ import FlairStore from '../../stores/FlairStore';
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
import {Group} from "matrix-js-sdk";
import {allSettled, sleep} from "../../utils/promise";
const LONG_DESC_PLACEHOLDER = _td(
`<h1>HTML for your community's page</h1>
@ -98,11 +98,10 @@ const CategoryRoomList = createReactClass({
onFinished: (success, addrs) => {
if (!success) return;
const errorList = [];
Promise.all(addrs.map((addr) => {
allSettled(addrs.map((addr) => {
return GroupStore
.addRoomToGroupSummary(this.props.groupId, addr.address)
.catch(() => { errorList.push(addr.address); })
.reflect();
.catch(() => { errorList.push(addr.address); });
})).then(() => {
if (errorList.length === 0) {
return;
@ -275,11 +274,10 @@ const RoleUserList = createReactClass({
onFinished: (success, addrs) => {
if (!success) return;
const errorList = [];
Promise.all(addrs.map((addr) => {
allSettled(addrs.map((addr) => {
return GroupStore
.addUserToGroupSummary(addr.address)
.catch(() => { errorList.push(addr.address); })
.reflect();
.catch(() => { errorList.push(addr.address); });
})).then(() => {
if (errorList.length === 0) {
return;
@ -638,7 +636,7 @@ export default createReactClass({
title: _t('Error'),
description: _t('Failed to upload image'),
});
}).done();
});
},
_onJoinableChange: function(ev) {
@ -677,7 +675,7 @@ export default createReactClass({
this.setState({
avatarChanged: false,
});
}).done();
});
},
_saveGroup: async function() {
@ -692,7 +690,7 @@ export default createReactClass({
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
// spinner disappearing after we have fetched new group data.
await Promise.delay(500);
await sleep(500);
GroupStore.acceptGroupInvite(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
@ -711,7 +709,7 @@ export default createReactClass({
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
// spinner disappearing after we have fetched new group data.
await Promise.delay(500);
await sleep(500);
GroupStore.leaveGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
@ -735,7 +733,7 @@ export default createReactClass({
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
// spinner disappearing after we have fetched new group data.
await Promise.delay(500);
await sleep(500);
GroupStore.joinGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
@ -787,7 +785,7 @@ export default createReactClass({
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
// spinner disappearing after we have fetched new group data.
await Promise.delay(500);
await sleep(500);
GroupStore.leaveGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync

View file

@ -121,7 +121,7 @@ export default createReactClass({
this.setState({
errorText: msg,
});
}).done();
});
this._intervalId = null;
if (this.props.poll) {

View file

@ -525,6 +525,7 @@ const LoggedInView = createReactClass({
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
const GroupView = sdk.getComponent('structures.GroupView');
const MyGroups = sdk.getComponent('structures.MyGroups');
const ToastContainer = sdk.getComponent('structures.ToastContainer');
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
const CookieBar = sdk.getComponent('globals.CookieBar');
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
@ -628,6 +629,7 @@ const LoggedInView = createReactClass({
return (
<div onPaste={this._onPaste} onKeyDown={this._onReactKeyDown} className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onMouseDown={this._onMouseDown} onMouseUp={this._onMouseUp}>
{ topBar }
<ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._setResizeContainerRef} className={bodyClasses}>
<LeftPanel

View file

@ -17,8 +17,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
@ -59,11 +57,10 @@ import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
import DMRoomMap from '../../utils/DMRoomMap';
import { countRoomsWithNotif } from '../../RoomNotifs';
import { setTheme } from "../../theme";
// Disable warnings for now: we use deprecated bluebird functions
// and need to migrate, but they spam the console with warnings.
Promise.config({warnings: false});
import { ThemeWatcher } from "../../theme";
import { storeRoomAliasInCache } from '../../RoomAliasCache';
import { defer } from "../../utils/promise";
import KeyVerificationStateObserver from '../../utils/KeyVerificationStateObserver';
/** constants for MatrixChat.state.view */
const VIEWS = {
@ -236,7 +233,7 @@ export default createReactClass({
// Used by _viewRoom before getting state from sync
this.firstSyncComplete = false;
this.firstSyncPromise = Promise.defer();
this.firstSyncPromise = defer();
if (this.props.config.sync_timeline_limit) {
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
@ -272,6 +269,8 @@ export default createReactClass({
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this._themeWatcher = new ThemeWatcher();
this._themeWatcher.start();
this.focusComposer = false;
@ -358,6 +357,7 @@ export default createReactClass({
componentWillUnmount: function() {
Lifecycle.stopMatrixClient();
dis.unregister(this.dispatcherRef);
this._themeWatcher.stop();
window.removeEventListener("focus", this.onFocus);
window.removeEventListener('resize', this.handleResize);
this.state.resizeNotifier.removeListener("middlePanelResized", this._dispatchTimelineResize);
@ -540,7 +540,7 @@ export default createReactClass({
const Loader = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
MatrixClientPeg.get().leave(payload.room_id).done(() => {
MatrixClientPeg.get().leave(payload.room_id).then(() => {
modal.close();
if (this.state.currentRoomId === payload.room_id) {
dis.dispatch({action: 'view_next_room'});
@ -627,6 +627,22 @@ export default createReactClass({
case 'view_invite':
showRoomInviteDialog(payload.roomId);
break;
case 'view_last_screen':
// This function does what we want, despite the name. The idea is that it shows
// the last room we were looking at or some reasonable default/guess. We don't
// have to worry about email invites or similar being re-triggered because the
// function will have cleared that state and not execute that path.
this._showScreenAfterLogin();
break;
case 'toggle_my_groups':
// We just dispatch the page change rather than have to worry about
// what the logic is for each of these branches.
if (this.state.page_type === PageTypes.MyGroups) {
dis.dispatch({action: 'view_last_screen'});
} else {
dis.dispatch({action: 'view_my_groups'});
}
break;
case 'notifier_enabled': {
this.setState({showNotifierToolbar: Notifier.shouldShowToolbar()});
}
@ -661,9 +677,6 @@ export default createReactClass({
});
break;
}
case 'set_theme':
setTheme(payload.value);
break;
case 'on_logging_in':
// We are now logging in, so set the state to reflect that
// NB. This does not touch 'ready' since if our dispatches
@ -861,12 +874,17 @@ export default createReactClass({
waitFor = this.firstSyncPromise.promise;
}
waitFor.done(() => {
waitFor.then(() => {
let presentedId = roomInfo.room_alias || roomInfo.room_id;
const room = MatrixClientPeg.get().getRoom(roomInfo.room_id);
if (room) {
const theAlias = Rooms.getDisplayAliasForRoom(room);
if (theAlias) presentedId = theAlias;
if (theAlias) {
presentedId = theAlias;
// Store display alias of the presented room in cache to speed future
// navigation.
storeRoomAliasInCache(theAlias, room.roomId);
}
// Store this as the ID of the last room accessed. This is so that we can
// persist which room is being stored across refreshes and browser quits.
@ -973,7 +991,7 @@ export default createReactClass({
const [shouldCreate, createOpts] = await modal.finished;
if (shouldCreate) {
createRoom({createOpts}).done();
createRoom({createOpts});
}
},
@ -1261,9 +1279,8 @@ export default createReactClass({
// since we're about to start the client and therefore about
// to do the first sync
this.firstSyncComplete = false;
this.firstSyncPromise = Promise.defer();
this.firstSyncPromise = defer();
const cli = MatrixClientPeg.get();
const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog');
// Allow the JS SDK to reap timeline events. This reduces the amount of
// memory consumed as the JS SDK stores multiple distinct copies of room
@ -1308,7 +1325,7 @@ export default createReactClass({
if (state === "SYNCING" && prevState === "SYNCING") {
return;
}
console.log("MatrixClient sync state => %s", state);
console.info("MatrixClient sync state => %s", state);
if (state !== "PREPARED") { return; }
self.firstSyncComplete = true;
@ -1369,17 +1386,6 @@ export default createReactClass({
}, null, true);
});
cli.on("accountData", function(ev) {
if (ev.getType() === 'im.vector.web.settings') {
if (ev.getContent() && ev.getContent().theme) {
dis.dispatch({
action: 'set_theme',
value: ev.getContent().theme,
});
}
}
});
const dft = new DecryptionFailureTracker((total, errorCode) => {
Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total);
}, (errorCode) => {
@ -1473,12 +1479,35 @@ export default createReactClass({
}
});
cli.on("crypto.verification.start", (verifier) => {
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
verifier,
});
});
if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
cli.on("crypto.verification.request", request => {
let requestObserver;
if (request.event.getRoomId()) {
requestObserver = new KeyVerificationStateObserver(
request.event, MatrixClientPeg.get());
}
if (!requestObserver || requestObserver.pending) {
dis.dispatch({
action: "show_toast",
toast: {
key: request.event.getId(),
title: _t("Verification Request"),
icon: "verification",
props: {request, requestObserver},
component: sdk.getComponent("toasts.VerificationRequestToast"),
},
});
}
});
} else {
cli.on("crypto.verification.start", (verifier) => {
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
verifier,
});
});
}
// Fire the tinter right on startup to ensure the default theme is applied
// A later sync can/will correct the tint to be the right value for the user
const colorScheme = SettingsStore.getValue("roomColor");
@ -1749,7 +1778,7 @@ export default createReactClass({
return;
}
cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => {
cli.sendEvent(roomId, event.getType(), event.getContent()).then(() => {
dis.dispatch({action: 'message_sent'});
}, (err) => {
dis.dispatch({action: 'message_send_failed'});
@ -1761,10 +1790,12 @@ export default createReactClass({
const client = MatrixClientPeg.get();
const room = client && client.getRoom(this.state.currentRoomId);
if (room) {
subtitle = `| ${ room.name } ${subtitle}`;
subtitle = `${this.subTitleStatus} | ${ room.name } ${subtitle}`;
}
} else {
subtitle = `${this.subTitleStatus} ${subtitle}`;
}
document.title = `${SdkConfig.get().brand || 'Riot'} ${subtitle} ${this.subTitleStatus}`;
document.title = `${SdkConfig.get().brand || 'Riot'} ${subtitle}`;
},
updateStatusIndicator: function(state, prevState) {

View file

@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,10 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/* global Velocity */
import React from 'react';
import createReactClass from 'create-react-class';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
@ -37,10 +35,8 @@ const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType()
/* (almost) stateless UI component which builds the event tiles in the room timeline.
*/
module.exports = createReactClass({
displayName: 'MessagePanel',
propTypes: {
export default class MessagePanel extends React.Component {
static propTypes = {
// true to give the component a 'display: none' style.
hidden: PropTypes.bool,
@ -109,17 +105,16 @@ module.exports = createReactClass({
// whether to show reactions for an event
showReactions: PropTypes.bool,
},
};
componentWillMount: function() {
// the event after which we put a visible unread marker on the last
// render cycle; null if readMarkerVisible was false or the RM was
// suppressed (eg because it was at the end of the timeline)
this.currentReadMarkerEventId = null;
constructor() {
super();
// the event after which we are showing a disappearing read marker
// animation
this.currentGhostEventId = null;
this.state = {
// previous positions the read marker has been in, so we can
// display 'ghost' read markers that are animating away
ghostReadMarkers: [],
};
// opaque readreceipt info for each userId; used by ReadReceiptMarker
// to manage its animations
@ -158,47 +153,57 @@ module.exports = createReactClass({
// displayed event in the current render cycle.
this._readReceiptsByUserId = {};
// Remember the read marker ghost node so we can do the cleanup that
// Velocity requires
this._readMarkerGhostNode = null;
// Cache hidden events setting on mount since Settings is expensive to
// query, and we check this in a hot code path.
this._showHiddenEventsInTimeline =
SettingsStore.getValue("showHiddenEventsInTimeline");
this._isMounted = true;
},
componentWillUnmount: function() {
this._isMounted = false;
},
}
componentDidMount() {
this._isMounted = true;
}
componentWillUnmount() {
this._isMounted = false;
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.readMarkerVisible && this.props.readMarkerEventId !== prevProps.readMarkerEventId) {
const ghostReadMarkers = this.state.ghostReadMarkers;
ghostReadMarkers.push(prevProps.readMarkerEventId);
this.setState({
ghostReadMarkers,
});
}
}
/* get the DOM node representing the given event */
getNodeForEventId: function(eventId) {
getNodeForEventId(eventId) {
if (!this.eventNodes) {
return undefined;
}
return this.eventNodes[eventId];
},
}
/* return true if the content is fully scrolled down right now; else false.
*/
isAtBottom: function() {
isAtBottom() {
return this.refs.scrollPanel
&& this.refs.scrollPanel.isAtBottom();
},
}
/* get the current scroll state. See ScrollPanel.getScrollState for
* details.
*
* returns null if we are not mounted.
*/
getScrollState: function() {
getScrollState() {
if (!this.refs.scrollPanel) { return null; }
return this.refs.scrollPanel.getScrollState();
},
}
// returns one of:
//
@ -206,7 +211,7 @@ module.exports = createReactClass({
// -1: read marker is above the window
// 0: read marker is within the window
// +1: read marker is below the window
getReadMarkerPosition: function() {
getReadMarkerPosition() {
const readMarker = this.refs.readMarkerNode;
const messageWrapper = this.refs.scrollPanel;
@ -226,45 +231,45 @@ module.exports = createReactClass({
} else {
return 1;
}
},
}
/* jump to the top of the content.
*/
scrollToTop: function() {
scrollToTop() {
if (this.refs.scrollPanel) {
this.refs.scrollPanel.scrollToTop();
}
},
}
/* jump to the bottom of the content.
*/
scrollToBottom: function() {
scrollToBottom() {
if (this.refs.scrollPanel) {
this.refs.scrollPanel.scrollToBottom();
}
},
}
/**
* Page up/down.
*
* @param {number} mult: -1 to page up, +1 to page down
*/
scrollRelative: function(mult) {
scrollRelative(mult) {
if (this.refs.scrollPanel) {
this.refs.scrollPanel.scrollRelative(mult);
}
},
}
/**
* Scroll up/down in response to a scroll key
*
* @param {KeyboardEvent} ev: the keyboard event to handle
*/
handleScrollKey: function(ev) {
handleScrollKey(ev) {
if (this.refs.scrollPanel) {
this.refs.scrollPanel.handleScrollKey(ev);
}
},
}
/* jump to the given event id.
*
@ -276,33 +281,33 @@ module.exports = createReactClass({
* node (specifically, the bottom of it) will be positioned. If omitted, it
* defaults to 0.
*/
scrollToEvent: function(eventId, pixelOffset, offsetBase) {
scrollToEvent(eventId, pixelOffset, offsetBase) {
if (this.refs.scrollPanel) {
this.refs.scrollPanel.scrollToToken(eventId, pixelOffset, offsetBase);
}
},
}
scrollToEventIfNeeded: function(eventId) {
scrollToEventIfNeeded(eventId) {
const node = this.eventNodes[eventId];
if (node) {
node.scrollIntoView({block: "nearest", behavior: "instant"});
}
},
}
/* check the scroll state and send out pagination requests if necessary.
*/
checkFillState: function() {
checkFillState() {
if (this.refs.scrollPanel) {
this.refs.scrollPanel.checkFillState();
}
},
}
_isUnmounting: function() {
_isUnmounting() {
return !this._isMounted;
},
}
// TODO: Implement granular (per-room) hide options
_shouldShowEvent: function(mxEv) {
_shouldShowEvent(mxEv) {
if (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) {
return false; // ignored = no show (only happens if the ignore happens after an event was received)
}
@ -320,16 +325,87 @@ module.exports = createReactClass({
if (this.props.highlightedEventId === mxEv.getId()) return true;
return !shouldHideEvent(mxEv);
},
}
_getEventTiles: function() {
_readMarkerForEvent(eventId, isLastEvent) {
const visible = !isLastEvent && this.props.readMarkerVisible;
if (this.props.readMarkerEventId === eventId) {
let hr;
// if the read marker comes at the end of the timeline (except
// for local echoes, which are excluded from RMs, because they
// don't have useful event ids), we don't want to show it, but
// we still want to create the <li/> for it so that the
// algorithms which depend on its position on the screen aren't
// confused.
if (visible) {
hr = <hr className="mx_RoomView_myReadMarker"
style={{opacity: 1, width: '99%'}}
/>;
}
return (
<li key={"readMarker_"+eventId} ref="readMarkerNode"
className="mx_RoomView_myReadMarker_container">
{ hr }
</li>
);
} else if (this.state.ghostReadMarkers.includes(eventId)) {
// We render 'ghost' read markers in the DOM while they
// transition away. This allows the actual read marker
// to be in the right place straight away without having
// to wait for the transition to finish.
// There are probably much simpler ways to do this transition,
// possibly using react-transition-group which handles keeping
// elements in the DOM whilst they transition out, although our
// case is a little more complex because only some of the items
// transition (ie. the read markers do but the event tiles do not)
// and TransitionGroup requires that all its children are Transitions.
const hr = <hr className="mx_RoomView_myReadMarker"
ref={this._collectGhostReadMarker}
onTransitionEnd={this._onGhostTransitionEnd}
data-eventid={eventId}
/>;
// give it a key which depends on the event id. That will ensure that
// we get a new DOM node (restarting the animation) when the ghost
// moves to a different event.
return (
<li key={"_readuptoghost_"+eventId}
className="mx_RoomView_myReadMarker_container">
{ hr }
</li>
);
}
return null;
}
_collectGhostReadMarker = (node) => {
if (node) {
// now the element has appeared, change the style which will trigger the CSS transition
requestAnimationFrame(() => {
node.style.width = '10%';
node.style.opacity = '0';
});
}
};
_onGhostTransitionEnd = (ev) => {
// we can now clean up the ghost element
const finishedEventId = ev.target.dataset.eventid;
this.setState({
ghostReadMarkers: this.state.ghostReadMarkers.filter(eid => eid !== finishedEventId),
});
};
_getEventTiles() {
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
this.eventNodes = {};
let visible = false;
let i;
// first figure out which is the last event in the list which we're
@ -364,16 +440,6 @@ module.exports = createReactClass({
let prevEvent = null; // the last event we showed
// assume there is no read marker until proven otherwise
let readMarkerVisible = false;
// if the readmarker has moved, cancel any active ghost.
if (this.currentReadMarkerEventId && this.props.readMarkerEventId &&
this.props.readMarkerVisible &&
this.currentReadMarkerEventId !== this.props.readMarkerEventId) {
this.currentGhostEventId = null;
}
this._readReceiptsByEvent = {};
if (this.props.showReadReceipts) {
this._readReceiptsByEvent = this._getReadReceiptsByShownEvent();
@ -398,7 +464,7 @@ module.exports = createReactClass({
return false;
};
if (mxEv.getType() === "m.room.create") {
let readMarkerInSummary = false;
let summaryReadMarker = null;
const ts1 = mxEv.getTs();
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
@ -407,8 +473,12 @@ module.exports = createReactClass({
}
// If RM event is the first in the summary, append the RM after the summary
if (mxEv.getId() === this.props.readMarkerEventId) {
readMarkerInSummary = true;
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId());
// If this m.room.create event should be shown (room upgrade) then show it before the summary
if (this._shouldShowEvent(mxEv)) {
// pass in the mxEv as prevEvent as well so no extra DateSeparator is rendered
ret.push(...this._getTilesForEvent(mxEv, mxEv, false));
}
const summarisedEvents = []; // Don't add m.room.create here as we don't want it inside the summary
@ -418,9 +488,7 @@ module.exports = createReactClass({
// Ignore redacted/hidden member events
if (!this._shouldShowEvent(collapsedMxEv)) {
// If this hidden event is the RM and in or at end of a summary put RM after the summary.
if (collapsedMxEv.getId() === this.props.readMarkerEventId) {
readMarkerInSummary = true;
}
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
continue;
}
@ -429,9 +497,7 @@ module.exports = createReactClass({
}
// If RM event is in the summary, mark it as such and the RM will be appended after the summary.
if (collapsedMxEv.getId() === this.props.readMarkerEventId) {
readMarkerInSummary = true;
}
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
summarisedEvents.push(collapsedMxEv);
}
@ -459,8 +525,8 @@ module.exports = createReactClass({
{ eventTiles }
</EventListSummary>);
if (readMarkerInSummary) {
ret.push(this._getReadMarkerTile(visible));
if (summaryReadMarker) {
ret.push(summaryReadMarker);
}
prevEvent = mxEv;
@ -471,7 +537,7 @@ module.exports = createReactClass({
// Wrap consecutive member events in a ListSummary, ignore if redacted
if (isMembershipChange(mxEv) && wantTile) {
let readMarkerInMels = false;
let summaryReadMarker = null;
const ts1 = mxEv.getTs();
// Ensure that the key of the MemberEventListSummary does not change with new
// member events. This will prevent it from being re-created unnecessarily, and
@ -489,9 +555,7 @@ module.exports = createReactClass({
}
// If RM event is the first in the MELS, append the RM after MELS
if (mxEv.getId() === this.props.readMarkerEventId) {
readMarkerInMels = true;
}
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId());
const summarisedEvents = [mxEv];
for (;i + 1 < this.props.events.length; i++) {
@ -500,9 +564,7 @@ module.exports = createReactClass({
// Ignore redacted/hidden member events
if (!this._shouldShowEvent(collapsedMxEv)) {
// If this hidden event is the RM and in or at end of a MELS put RM after MELS.
if (collapsedMxEv.getId() === this.props.readMarkerEventId) {
readMarkerInMels = true;
}
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
continue;
}
@ -512,9 +574,7 @@ module.exports = createReactClass({
}
// If RM event is in MELS mark it as such and the RM will be appended after MELS.
if (collapsedMxEv.getId() === this.props.readMarkerEventId) {
readMarkerInMels = true;
}
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
summarisedEvents.push(collapsedMxEv);
}
@ -545,8 +605,8 @@ module.exports = createReactClass({
{ eventTiles }
</MemberEventListSummary>);
if (readMarkerInMels) {
ret.push(this._getReadMarkerTile(visible));
if (summaryReadMarker) {
ret.push(summaryReadMarker);
}
prevEvent = mxEv;
@ -561,44 +621,14 @@ module.exports = createReactClass({
prevEvent = mxEv;
}
let isVisibleReadMarker = false;
if (eventId === this.props.readMarkerEventId) {
visible = this.props.readMarkerVisible;
// if the read marker comes at the end of the timeline (except
// for local echoes, which are excluded from RMs, because they
// don't have useful event ids), we don't want to show it, but
// we still want to create the <li/> for it so that the
// algorithms which depend on its position on the screen aren't
// confused.
if (i >= lastShownNonLocalEchoIndex) {
visible = false;
}
ret.push(this._getReadMarkerTile(visible));
readMarkerVisible = visible;
isVisibleReadMarker = visible;
}
// XXX: there should be no need for a ghost tile - we should just use a
// a dispatch (user_activity_end) to start the RM animation.
if (eventId === this.currentGhostEventId) {
// if we're showing an animation, continue to show it.
ret.push(this._getReadMarkerGhostTile());
} else if (!isVisibleReadMarker &&
eventId === this.currentReadMarkerEventId) {
// there is currently a read-up-to marker at this point, but no
// more. Show an animation of it disappearing.
ret.push(this._getReadMarkerGhostTile());
this.currentGhostEventId = eventId;
}
const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
if (readMarker) ret.push(readMarker);
}
this.currentReadMarkerEventId = readMarkerVisible ? this.props.readMarkerEventId : null;
return ret;
},
}
_getTilesForEvent: function(prevEvent, mxEv, last) {
_getTilesForEvent(prevEvent, mxEv, last) {
const EventTile = sdk.getComponent('rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const ret = [];
@ -691,20 +721,20 @@ module.exports = createReactClass({
);
return ret;
},
}
_wantsDateSeparator: function(prevEvent, nextEventDate) {
_wantsDateSeparator(prevEvent, nextEventDate) {
if (prevEvent == null) {
// first event in the panel: depends if we could back-paginate from
// here.
return !this.props.suppressFirstDateSeparator;
}
return wantsDateSeparator(prevEvent.getDate(), nextEventDate);
},
}
// Get a list of read receipts that should be shown next to this event
// Receipts are objects which have a 'userId', 'roomMember' and 'ts'.
_getReadReceiptsForEvent: function(event) {
_getReadReceiptsForEvent(event) {
const myUserId = MatrixClientPeg.get().credentials.userId;
// get list of read receipts, sorted most recent first
@ -728,12 +758,12 @@ module.exports = createReactClass({
});
});
return receipts;
},
}
// Get an object that maps from event ID to a list of read receipts that
// should be shown next to that event. If a hidden event has read receipts,
// they are folded into the receipts of the last shown event.
_getReadReceiptsByShownEvent: function() {
_getReadReceiptsByShownEvent() {
const receiptsByEvent = {};
const receiptsByUserId = {};
@ -786,78 +816,31 @@ module.exports = createReactClass({
}
return receiptsByEvent;
},
}
_getReadMarkerTile: function(visible) {
let hr;
if (visible) {
hr = <hr className="mx_RoomView_myReadMarker"
style={{opacity: 1, width: '99%'}}
/>;
}
return (
<li key="_readupto" ref="readMarkerNode"
className="mx_RoomView_myReadMarker_container">
{ hr }
</li>
);
},
_startAnimation: function(ghostNode) {
if (this._readMarkerGhostNode) {
Velocity.Utilities.removeData(this._readMarkerGhostNode);
}
this._readMarkerGhostNode = ghostNode;
if (ghostNode) {
// eslint-disable-next-line new-cap
Velocity(ghostNode, {opacity: '0', width: '10%'},
{duration: 400, easing: 'easeInSine',
delay: 1000});
}
},
_getReadMarkerGhostTile: function() {
const hr = <hr className="mx_RoomView_myReadMarker"
style={{opacity: 1, width: '99%'}}
ref={this._startAnimation}
/>;
// give it a key which depends on the event id. That will ensure that
// we get a new DOM node (restarting the animation) when the ghost
// moves to a different event.
return (
<li key={"_readuptoghost_"+this.currentGhostEventId}
className="mx_RoomView_myReadMarker_container">
{ hr }
</li>
);
},
_collectEventNode: function(eventId, node) {
_collectEventNode = (eventId, node) => {
this.eventNodes[eventId] = node;
},
}
// once dynamic content in the events load, make the scrollPanel check the
// scroll offsets.
_onHeightChanged: function() {
_onHeightChanged = () => {
const scrollPanel = this.refs.scrollPanel;
if (scrollPanel) {
scrollPanel.checkScroll();
}
},
};
_onTypingShown: function() {
_onTypingShown = () => {
const scrollPanel = this.refs.scrollPanel;
// this will make the timeline grow, so checkScroll
scrollPanel.checkScroll();
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
scrollPanel.preventShrinking();
}
},
};
_onTypingHidden: function() {
_onTypingHidden = () => {
const scrollPanel = this.refs.scrollPanel;
if (scrollPanel) {
// as hiding the typing notifications doesn't
@ -868,9 +851,9 @@ module.exports = createReactClass({
// reveal added padding to balance the notifs disappearing.
scrollPanel.checkScroll();
}
},
};
updateTimelineMinHeight: function() {
updateTimelineMinHeight() {
const scrollPanel = this.refs.scrollPanel;
if (scrollPanel) {
@ -885,16 +868,16 @@ module.exports = createReactClass({
scrollPanel.preventShrinking();
}
}
},
}
onTimelineReset: function() {
onTimelineReset() {
const scrollPanel = this.refs.scrollPanel;
if (scrollPanel) {
scrollPanel.clearPreventShrinking();
}
},
}
render: function() {
render() {
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile");
const Spinner = sdk.getComponent("elements.Spinner");
@ -941,5 +924,5 @@ module.exports = createReactClass({
{ bottomSpinner }
</ScrollPanel>
);
},
});
}
}

View file

@ -47,7 +47,7 @@ export default createReactClass({
},
_fetch: function() {
this.context.matrixClient.getJoinedGroups().done((result) => {
this.context.matrixClient.getJoinedGroups().then((result) => {
this.setState({groups: result.groups, error: null});
}, (err) => {
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {

View file

@ -185,7 +185,7 @@ export default class RightPanel extends React.Component {
} else if (this.state.phase === RightPanel.Phase.GroupRoomList) {
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
} else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) {
if (SettingsStore.isFeatureEnabled("feature_user_info_panel")) {
if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
const onClose = () => {
dis.dispatch({
action: "view_user",
@ -204,7 +204,7 @@ export default class RightPanel extends React.Component {
} else if (this.state.phase === RightPanel.Phase.Room3pidMemberInfo) {
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
} else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) {
if (SettingsStore.isFeatureEnabled("feature_user_info_panel")) {
if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
const onClose = () => {
dis.dispatch({
action: "view_user",

View file

@ -27,7 +27,6 @@ const dis = require('../../dispatcher');
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
import PropTypes from 'prop-types';
import Promise from 'bluebird';
import { _t } from '../../languageHandler';
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
import Analytics from '../../Analytics';
@ -89,7 +88,7 @@ module.exports = createReactClass({
this.setState({protocolsLoading: false});
return;
}
MatrixClientPeg.get().getThirdpartyProtocols().done((response) => {
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
this.protocols = response;
this.setState({protocolsLoading: false});
}, (err) => {
@ -135,7 +134,7 @@ module.exports = createReactClass({
publicRooms: [],
loading: true,
});
this.getMoreRooms().done();
this.getMoreRooms();
},
getMoreRooms: function() {
@ -246,7 +245,7 @@ module.exports = createReactClass({
if (!alias) return;
step = _t('delete the alias.');
return MatrixClientPeg.get().deleteAlias(alias);
}).done(() => {
}).then(() => {
modal.close();
this.refreshRoomList();
}, (err) => {
@ -348,7 +347,7 @@ module.exports = createReactClass({
});
return;
}
MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).done((resp) => {
MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).then((resp) => {
if (resp.length > 0 && resp[0].alias) {
this.showRoomAlias(resp[0].alias, true);
} else {

View file

@ -289,7 +289,7 @@ module.exports = createReactClass({
}
return <div className="mx_RoomStatusBar_connectionLostBar">
<img src={require("../../../res/img/e2e/warning.svg")} width="24" height="24" title={_t("Warning")} alt="" />
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24" height="24" title={_t("Warning")} alt="" />
<div>
<div className="mx_RoomStatusBar_connectionLostBar_title">
{ title }
@ -306,7 +306,7 @@ module.exports = createReactClass({
if (this._shouldShowConnectionError()) {
return (
<div className="mx_RoomStatusBar_connectionLostBar">
<img src={require("../../../res/img/e2e/warning.svg")} width="24" height="24" title="/!\ " alt="/!\ " />
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24" height="24" title="/!\ " alt="/!\ " />
<div>
<div className="mx_RoomStatusBar_connectionLostBar_title">
{ _t('Connectivity to the server has been lost.') }

View file

@ -27,7 +27,6 @@ import React from 'react';
import createReactClass from 'create-react-class';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import Promise from 'bluebird';
import classNames from 'classnames';
import {Room} from "matrix-js-sdk";
import { _t } from '../../languageHandler';
@ -43,6 +42,7 @@ import Tinter from '../../Tinter';
import rate_limited_func from '../../ratelimitedfunc';
import ObjectUtils from '../../ObjectUtils';
import * as Rooms from '../../Rooms';
import eventSearch from '../../Searching';
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
@ -357,7 +357,7 @@ module.exports = createReactClass({
if (this.props.autoJoin) {
this.onJoinButtonClicked();
} else if (!room && shouldPeek) {
console.log("Attempting to peek into room %s", roomId);
console.info("Attempting to peek into room %s", roomId);
this.setState({
peekLoading: true,
isPeeking: true, // this will change to false if peeking fails
@ -1101,7 +1101,7 @@ module.exports = createReactClass({
}
ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get())
.done(undefined, (error) => {
.then(undefined, (error) => {
if (error.name === "UnknownDeviceError") {
// Let the staus bar handle this
return;
@ -1129,23 +1129,12 @@ module.exports = createReactClass({
// todo: should cancel any previous search requests.
this.searchId = new Date().getTime();
let filter;
if (scope === "Room") {
filter = {
// XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :(
rooms: [
this.state.room.roomId,
],
};
}
let roomId;
if (scope === "Room") roomId = this.state.room.roomId;
debuglog("sending search request");
const searchPromise = MatrixClientPeg.get().searchRoomEvents({
filter: filter,
term: term,
});
this._handleSearchResult(searchPromise).done();
const searchPromise = eventSearch(term, roomId);
this._handleSearchResult(searchPromise);
},
_handleSearchResult: function(searchPromise) {
@ -1316,7 +1305,7 @@ module.exports = createReactClass({
},
onForgetClick: function() {
MatrixClientPeg.get().forget(this.state.room.roomId).done(function() {
MatrixClientPeg.get().forget(this.state.room.roomId).then(function() {
dis.dispatch({ action: 'view_next_room' });
}, function(err) {
const errCode = err.errcode || _t("unknown error code");
@ -1333,7 +1322,7 @@ module.exports = createReactClass({
this.setState({
rejecting: true,
});
MatrixClientPeg.get().leave(this.state.roomId).done(function() {
MatrixClientPeg.get().leave(this.state.roomId).then(function() {
dis.dispatch({ action: 'view_next_room' });
self.setState({
rejecting: false,
@ -1907,7 +1896,7 @@ module.exports = createReactClass({
highlightedEventId = this.state.initialEventId;
}
// console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
// console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
const messagePanel = (
<TimelinePanel ref={this._gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()}

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from "react";
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import Promise from 'bluebird';
import { KeyCode } from '../../Keyboard';
import Timer from '../../utils/Timer';
import AutoHideScrollbar from "./AutoHideScrollbar";

View file

@ -23,7 +23,6 @@ import React from 'react';
import createReactClass from 'create-react-class';
import ReactDOM from "react-dom";
import PropTypes from 'prop-types';
import Promise from 'bluebird';
const Matrix = require("matrix-js-sdk");
const EventTimeline = Matrix.EventTimeline;
@ -462,7 +461,7 @@ const TimelinePanel = createReactClass({
// timeline window.
//
// see https://github.com/vector-im/vector-web/issues/1035
this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => {
this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => {
if (this.unmounted) { return; }
const { events, liveEvents } = this._getEvents();
@ -1064,8 +1063,6 @@ const TimelinePanel = createReactClass({
});
};
let prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
// if we already have the event in question, TimelineWindow.load
// returns a resolved promise.
//
@ -1074,9 +1071,14 @@ const TimelinePanel = createReactClass({
// quite slow. So we detect that situation and shortcut straight to
// calling _reloadEvents and updating the state.
if (prom.isFulfilled()) {
const timeline = this.props.timelineSet.getTimelineForEvent(eventId);
if (timeline) {
// This is a hot-path optimization by skipping a promise tick
// by repeating a no-op sync branch in TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline
this._timelineWindow.load(eventId, INITIAL_SIZE); // in this branch this method will happen in sync time
onLoaded();
} else {
const prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
this.setState({
events: [],
liveEvents: [],
@ -1084,11 +1086,8 @@ const TimelinePanel = createReactClass({
canForwardPaginate: false,
timelineLoading: true,
});
prom = prom.then(onLoaded, onError);
prom.then(onLoaded, onError);
}
prom.done();
},
// handle the completion of a timeline load or localEchoUpdate, by

View file

@ -0,0 +1,84 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as React from "react";
import dis from "../../dispatcher";
import { _t } from '../../languageHandler';
import classNames from "classnames";
export default class ToastContainer extends React.Component {
constructor() {
super();
this.state = {toasts: []};
}
componentDidMount() {
this._dispatcherRef = dis.register(this.onAction);
}
componentWillUnmount() {
dis.unregister(this._dispatcherRef);
}
onAction = (payload) => {
if (payload.action === "show_toast") {
this._addToast(payload.toast);
}
};
_addToast(toast) {
this.setState({toasts: this.state.toasts.concat(toast)});
}
dismissTopToast = () => {
const [, ...remaining] = this.state.toasts;
this.setState({toasts: remaining});
};
render() {
const totalCount = this.state.toasts.length;
const isStacked = totalCount > 1;
let toast;
if (totalCount !== 0) {
const topToast = this.state.toasts[0];
const {title, icon, key, component, props} = topToast;
const toastClasses = classNames("mx_Toast_toast", {
"mx_Toast_hasIcon": icon,
[`mx_Toast_icon_${icon}`]: icon,
});
const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null;
const toastProps = Object.assign({}, props, {
dismiss: this.dismissTopToast,
key,
});
toast = (<div className={toastClasses}>
<h2>{title}{countIndicator}</h2>
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
</div>);
}
const containerClasses = classNames("mx_ToastContainer", {
"mx_ToastContainer_stacked": isStacked,
});
return (
<div className={containerClasses} role="alert">
{toast}
</div>
);
}
}

View file

@ -105,7 +105,7 @@ module.exports = createReactClass({
phase: PHASE_SENDING_EMAIL,
});
this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl);
this.reset.resetPassword(email, password).done(() => {
this.reset.resetPassword(email, password).then(() => {
this.setState({
phase: PHASE_EMAIL_SENT,
});

View file

@ -253,7 +253,7 @@ module.exports = createReactClass({
this.setState({
busy: false,
});
}).done();
});
},
onUsernameChanged: function(username) {
@ -378,15 +378,30 @@ module.exports = createReactClass({
// Do a quick liveliness check on the URLs
try {
await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
this.setState({serverIsAlive: true, errorText: ""});
const { warning } =
await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
if (warning) {
this.setState({
...AutoDiscoveryUtils.authComponentStateForError(warning),
errorText: "",
});
} else {
this.setState({
serverIsAlive: true,
errorText: "",
});
}
} catch (e) {
this.setState({
busy: false,
...AutoDiscoveryUtils.authComponentStateForError(e),
});
if (this.state.serverErrorIsFatal) {
return; // Server is dead - do not continue.
// Server is dead: show server details prompt instead
this.setState({
phase: PHASE_SERVER_DETAILS,
});
return;
}
}
@ -424,7 +439,7 @@ module.exports = createReactClass({
this.setState({
busy: false,
});
}).done();
});
},
_isSupportedFlow: function(flow) {

View file

@ -43,7 +43,7 @@ module.exports = createReactClass({
const cli = MatrixClientPeg.get();
this.setState({busy: true});
const self = this;
cli.getProfileInfo(cli.credentials.userId).done(function(result) {
cli.getProfileInfo(cli.credentials.userId).then(function(result) {
self.setState({
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url),
busy: false,

View file

@ -18,7 +18,6 @@ limitations under the License.
*/
import Matrix from 'matrix-js-sdk';
import Promise from 'bluebird';
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
@ -371,7 +370,7 @@ module.exports = createReactClass({
if (pushers[i].kind === 'email') {
const emailPusher = pushers[i];
emailPusher.data = { brand: this.props.brand };
matrixClient.setPusher(emailPusher).done(() => {
matrixClient.setPusher(emailPusher).then(() => {
console.log("Set email branding to " + this.props.brand);
}, (error) => {
console.error("Couldn't set email branding: " + error);

View file

@ -89,7 +89,7 @@ module.exports = createReactClass({
+ "authentication");
}
console.log("Rendering to %s", divId);
console.info("Rendering to %s", divId);
this._captchaWidgetId = global.grecaptcha.render(divId, {
sitekey: publicKey,
callback: this.props.onCaptchaResponse,

View file

@ -441,7 +441,7 @@ export const MsisdnAuthEntry = createReactClass({
this.props.fail(e);
}).finally(() => {
this.setState({requestingToken: false});
}).done();
});
},
/*

View file

@ -17,7 +17,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
@ -32,6 +31,7 @@ import * as RoomNotifs from '../../../RoomNotifs';
import Modal from '../../../Modal';
import RoomListActions from '../../../actions/RoomListActions';
import RoomViewStore from '../../../stores/RoomViewStore';
import {sleep} from "../../../utils/promise";
import {MenuItem, MenuItemCheckbox, MenuItemRadio} from "../../structures/ContextualMenu";
const RoomTagOption = ({active, onClick, src, srcSet, label}) => {
@ -92,7 +92,7 @@ module.exports = createReactClass({
_toggleTag: function(tagNameOn, tagNameOff) {
if (!MatrixClientPeg.get().isGuest()) {
Promise.delay(500).then(() => {
sleep(500).then(() => {
dis.dispatch(RoomListActions.tagRoom(
MatrixClientPeg.get(),
this.props.room,
@ -149,7 +149,7 @@ module.exports = createReactClass({
Rooms.guessAndSetDMRoom(
this.props.room, newIsDirectMessage,
).delay(500).finally(() => {
).then(sleep(500)).finally(() => {
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
@ -190,7 +190,7 @@ module.exports = createReactClass({
_onClickForget: function() {
// FIXME: duplicated with RoomSettings (and dead code in RoomView)
MatrixClientPeg.get().forget(this.props.room.roomId).done(() => {
MatrixClientPeg.get().forget(this.props.room.roomId).then(() => {
// Switch to another room view if we're currently viewing the
// historical room
if (RoomViewStore.getRoomId() === this.props.room.roomId) {
@ -220,10 +220,10 @@ module.exports = createReactClass({
this.setState({
roomNotifState: newState,
});
RoomNotifs.setRoomNotifsState(roomId, newState).done(() => {
RoomNotifs.setRoomNotifsState(roomId, newState).then(() => {
// delay slightly so that the user can see their state change
// before closing the menu
return Promise.delay(500).then(() => {
return sleep(500).then(() => {
if (this._unmounted) return;
// Close the context menu
if (this.props.onFinished) {

View file

@ -0,0 +1,134 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import {_t} from '../../../languageHandler';
export default class WidgetContextMenu extends React.Component {
static propTypes = {
onFinished: PropTypes.func,
// Callback for when the revoke button is clicked. Required.
onRevokeClicked: PropTypes.func.isRequired,
// Callback for when the snapshot button is clicked. Button not shown
// without a callback.
onSnapshotClicked: PropTypes.func,
// Callback for when the reload button is clicked. Button not shown
// without a callback.
onReloadClicked: PropTypes.func,
// Callback for when the edit button is clicked. Button not shown
// without a callback.
onEditClicked: PropTypes.func,
// Callback for when the delete button is clicked. Button not shown
// without a callback.
onDeleteClicked: PropTypes.func,
};
proxyClick(fn) {
fn();
if (this.props.onFinished) this.props.onFinished();
}
// XXX: It's annoying that our context menus require us to hit onFinished() to close :(
onEditClicked = () => {
this.proxyClick(this.props.onEditClicked);
};
onReloadClicked = () => {
this.proxyClick(this.props.onReloadClicked);
};
onSnapshotClicked = () => {
this.proxyClick(this.props.onSnapshotClicked);
};
onDeleteClicked = () => {
this.proxyClick(this.props.onDeleteClicked);
};
onRevokeClicked = () => {
this.proxyClick(this.props.onRevokeClicked);
};
render() {
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
const options = [];
if (this.props.onEditClicked) {
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onEditClicked} key='edit'>
{_t("Edit")}
</AccessibleButton>,
);
}
if (this.props.onReloadClicked) {
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onReloadClicked}
key='reload'>
{_t("Reload")}
</AccessibleButton>,
);
}
if (this.props.onSnapshotClicked) {
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onSnapshotClicked}
key='snap'>
{_t("Take picture")}
</AccessibleButton>,
);
}
if (this.props.onDeleteClicked) {
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onDeleteClicked}
key='delete'>
{_t("Remove for everyone")}
</AccessibleButton>,
);
}
// Push this last so it appears last. It's always present.
options.push(
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onRevokeClicked} key='revoke'>
{_t("Remove for me")}
</AccessibleButton>,
);
// Put separators between the options
if (options.length > 1) {
const length = options.length;
for (let i = 0; i < length - 1; i++) {
const sep = <hr key={i} className="mx_WidgetContextMenu_separator" />;
// Insert backwards so the insertions don't affect our math on where to place them.
// We also use our cached length to avoid worrying about options.length changing
options.splice(length - 1 - i, 0, sep);
}
}
return <div className="mx_WidgetContextMenu">{options}</div>;
}
}

View file

@ -25,13 +25,13 @@ import { _t, _td } from '../../../languageHandler';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import dis from '../../../dispatcher';
import Promise from 'bluebird';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStore from '../../../stores/GroupStore';
import * as Email from '../../../email';
import IdentityAuthClient from '../../../IdentityAuthClient';
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils';
import { abbreviateUrl } from '../../../utils/UrlUtils';
import {sleep} from "../../../utils/promise";
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@ -266,7 +266,7 @@ module.exports = createReactClass({
this.setState({
searchError: err.errcode ? err.message : _t('Something went wrong!'),
});
}).done(() => {
}).then(() => {
this.setState({
busy: false,
});
@ -379,7 +379,7 @@ module.exports = createReactClass({
// Do a local search immediately
this._doLocalSearch(query);
}
}).done(() => {
}).then(() => {
this.setState({
busy: false,
});
@ -533,7 +533,7 @@ module.exports = createReactClass({
};
// wait a bit to let the user finish typing
await Promise.delay(500);
await sleep(500);
if (cancelled) return null;
try {

View file

@ -93,7 +93,7 @@ export default createReactClass({
this.setState({createError: e});
}).finally(() => {
this.setState({creating: false});
}).done();
});
},
_onCancel: function() {

View file

@ -0,0 +1,57 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import sdk from "../../../index";
import dis from '../../../dispatcher';
export default class IntegrationsDisabledDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
_onAcknowledgeClick = () => {
this.props.onFinished();
};
_onOpenSettingsClick = () => {
this.props.onFinished();
dis.dispatch({action: "view_user_settings"});
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className='mx_IntegrationsDisabledDialog' hasCancel={true}
onFinished={this.props.onFinished}
title={_t("Integrations are disabled")}>
<div className='mx_IntegrationsDisabledDialog_content'>
<p>{_t("Enable 'Manage Integrations' in Settings to do this.")}</p>
</div>
<DialogButtons
primaryButton={_t("Settings")}
onPrimaryButtonClick={this._onOpenSettingsClick}
cancelButton={_t("OK")}
onCancel={this._onAcknowledgeClick}
/>
</BaseDialog>
);
}
}

View file

@ -0,0 +1,55 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import sdk from "../../../index";
export default class IntegrationsImpossibleDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
_onAcknowledgeClick = () => {
this.props.onFinished();
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className='mx_IntegrationsImpossibleDialog' hasCancel={false}
onFinished={this.props.onFinished}
title={_t("Integrations not allowed")}>
<div className='mx_IntegrationsImpossibleDialog_content'>
<p>
{_t(
"Your Riot doesn't allow you to use an Integration Manager to do this. " +
"Please contact an admin.",
)}
</p>
</div>
<DialogButtons
primaryButton={_t("OK")}
onPrimaryButtonClick={this._onAcknowledgeClick}
hasCancel={false}
/>
</BaseDialog>
);
}
}

View file

@ -78,7 +78,7 @@ export default createReactClass({
true,
);
}
}).done();
});
},
componentWillUnmount: function() {

View file

@ -116,7 +116,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
nodes.push((
<EditHistoryMessage
key={e.getId()}
previousEdit={!isBaseEvent && allEvents[i + 1]}
previousEdit={!isBaseEvent ? allEvents[i + 1] : null}
isBaseEvent={isBaseEvent}
mxEvent={e}
isTwelveHour={this.state.isTwelveHour}

View file

@ -62,7 +62,7 @@ export default createReactClass({
return;
}
this._addThreepid = new AddThreepid();
this._addThreepid.addEmailAddress(emailAddress).done(() => {
this._addThreepid.addEmailAddress(emailAddress).then(() => {
Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, {
title: _t("Verification Pending"),
description: _t(
@ -96,7 +96,7 @@ export default createReactClass({
},
verifyEmailAddress: function() {
this._addThreepid.checkEmailLinkClicked().done(() => {
this._addThreepid.checkEmailLinkClicked().then(() => {
this.props.onFinished(true);
}, (err) => {
this.setState({emailBusy: false});

View file

@ -15,7 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';

View file

@ -82,10 +82,10 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
client.setTermsInteractionCallback((policyInfo, agreedUrls) => {
// To avoid visual glitching of two modals stacking briefly, we customise the
// terms dialog sizing when it will appear for the integrations manager so that
// terms dialog sizing when it will appear for the integration manager so that
// it gets the same basic size as the IM's own modal.
return dialogTermsInteractionCallback(
policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationsManager',
policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationManager',
);
});
@ -139,7 +139,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
}
_renderTab() {
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager");
let uiUrl = null;
if (this.state.currentScalarClient) {
uiUrl = this.state.currentScalarClient.getScalarInterfaceUrlForRoom(
@ -148,7 +148,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
this.props.integrationId,
);
}
return <IntegrationsManager
return <IntegrationManager
configured={true}
loading={this.state.currentLoading}
connected={this.state.currentConnected}

View file

@ -86,7 +86,7 @@ export default class TermsDialog extends React.PureComponent {
case Matrix.SERVICE_TYPES.IS:
return <div>{_t("Identity Server")}<br />({host})</div>;
case Matrix.SERVICE_TYPES.IM:
return <div>{_t("Integrations Manager")}<br />({host})</div>;
return <div>{_t("Integration Manager")}<br />({host})</div>;
}
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -29,12 +30,34 @@ import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab";
import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab";
import sdk from "../../../index";
import SdkConfig from "../../../SdkConfig";
import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
export default class UserSettingsDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
constructor() {
super();
this.state = {
mjolnirEnabled: SettingsStore.isFeatureEnabled("feature_mjolnir"),
};
}
componentDidMount(): void {
this._mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this._mjolnirChanged.bind(this));
}
componentWillUnmount(): void {
SettingsStore.unwatchSetting(this._mjolnirWatcher);
}
_mjolnirChanged(settingName, roomId, atLevel, newValue) {
// We can cheat because we know what levels a feature is tracked at, and how it is tracked
this.setState({mjolnirEnabled: newValue});
}
_getTabs() {
const tabs = [];
@ -75,6 +98,13 @@ export default class UserSettingsDialog extends React.Component {
<LabsUserSettingsTab />,
));
}
if (this.state.mjolnirEnabled) {
tabs.push(new Tab(
_td("Ignored users"),
"mx_UserSettingsDialog_mjolnirIcon",
<MjolnirUserSettingsTab />,
));
}
tabs.push(new Tab(
_td("Help & About"),
"mx_UserSettingsDialog_helpIcon",

View file

@ -19,79 +19,126 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import url from 'url';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import WidgetUtils from "../../../utils/WidgetUtils";
import MatrixClientPeg from "../../../MatrixClientPeg";
export default class AppPermission extends React.Component {
static propTypes = {
url: PropTypes.string.isRequired,
creatorUserId: PropTypes.string.isRequired,
roomId: PropTypes.string.isRequired,
onPermissionGranted: PropTypes.func.isRequired,
isRoomEncrypted: PropTypes.bool,
};
static defaultProps = {
onPermissionGranted: () => {},
};
constructor(props) {
super(props);
const curlBase = this.getCurlBase();
this.state = { curlBase: curlBase};
// The first step is to pick apart the widget so we can render information about it
const urlInfo = this.parseWidgetUrl();
// The second step is to find the user's profile so we can show it on the prompt
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
let roomMember;
if (room) roomMember = room.getMember(this.props.creatorUserId);
// Set all this into the initial state
this.state = {
...urlInfo,
roomMember,
};
}
// Return string representation of content URL without query parameters
getCurlBase() {
const wurl = url.parse(this.props.url);
let curl;
let curlString;
parseWidgetUrl() {
const widgetUrl = url.parse(this.props.url);
const params = new URLSearchParams(widgetUrl.search);
const searchParams = new URLSearchParams(wurl.search);
if (WidgetUtils.isScalarUrl(wurl) && searchParams && searchParams.get('url')) {
curl = url.parse(searchParams.get('url'));
if (curl) {
curl.search = curl.query = "";
curlString = curl.format();
}
// HACK: We're relying on the query params when we should be relying on the widget's `data`.
// This is a workaround for Scalar.
if (WidgetUtils.isScalarUrl(widgetUrl) && params && params.get('url')) {
const unwrappedUrl = url.parse(params.get('url'));
return {
widgetDomain: unwrappedUrl.host || unwrappedUrl.hostname,
isWrapped: true,
};
} else {
return {
widgetDomain: widgetUrl.host || widgetUrl.hostname,
isWrapped: false,
};
}
if (!curl && wurl) {
wurl.search = wurl.query = "";
curlString = wurl.format();
}
return curlString;
}
render() {
let e2eWarningText;
if (this.props.isRoomEncrypted) {
e2eWarningText =
<span className='mx_AppPermissionWarningTextLabel'>{ _t('NOTE: Apps are not end-to-end encrypted') }</span>;
}
const cookieWarning =
<span className='mx_AppPermissionWarningTextLabel'>
{ _t('Warning: This widget might use cookies.') }
</span>;
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
const TextWithTooltip = sdk.getComponent("views.elements.TextWithTooltip");
const displayName = this.state.roomMember ? this.state.roomMember.name : this.props.creatorUserId;
const userId = displayName === this.props.creatorUserId ? null : this.props.creatorUserId;
const avatar = this.state.roomMember
? <MemberAvatar member={this.state.roomMember} width={38} height={38} />
: <BaseAvatar name={this.props.creatorUserId} width={38} height={38} />;
const warningTooltipText = (
<div>
{_t("Any of the following data may be shared:")}
<ul>
<li>{_t("Your display name")}</li>
<li>{_t("Your avatar URL")}</li>
<li>{_t("Your user ID")}</li>
<li>{_t("Your theme")}</li>
<li>{_t("Riot URL")}</li>
<li>{_t("Room ID")}</li>
<li>{_t("Widget ID")}</li>
</ul>
</div>
);
const warningTooltip = (
<TextWithTooltip tooltip={warningTooltipText} tooltipClass='mx_AppPermissionWarning_tooltip mx_Tooltip_dark'>
<span className='mx_AppPermissionWarning_helpIcon' />
</TextWithTooltip>
);
// Due to i18n limitations, we can't dedupe the code for variables in these two messages.
const warning = this.state.isWrapped
? _t("Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.",
{widgetDomain: this.state.widgetDomain}, {helpIcon: () => warningTooltip})
: _t("Using this widget may share data <helpIcon /> with %(widgetDomain)s.",
{widgetDomain: this.state.widgetDomain}, {helpIcon: () => warningTooltip});
const encryptionWarning = this.props.isRoomEncrypted ? _t("Widgets do not use message encryption.") : null;
return (
<div className='mx_AppPermissionWarning'>
<div className='mx_AppPermissionWarningImage'>
<img src={require("../../../../res/img/feather-customised/warning-triangle.svg")} alt={_t('Warning!')} />
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_bolder mx_AppPermissionWarning_smallText'>
{_t("Widget added by")}
</div>
<div className='mx_AppPermissionWarningText'>
<span className='mx_AppPermissionWarningTextLabel'>{_t('Do you want to load widget from URL:')}</span>
<span className='mx_AppPermissionWarningTextURL'
title={this.state.curlBase}
>{this.state.curlBase}</span>
{ e2eWarningText }
{ cookieWarning }
<div className='mx_AppPermissionWarning_row'>
{avatar}
<h4 className='mx_AppPermissionWarning_bolder'>{displayName}</h4>
<div className='mx_AppPermissionWarning_smallText'>{userId}</div>
</div>
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_smallText'>
{warning}
</div>
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_smallText'>
{_t("This widget may use cookies.")}&nbsp;{encryptionWarning}
</div>
<div className='mx_AppPermissionWarning_row'>
<AccessibleButton kind='primary_sm' onClick={this.props.onPermissionGranted}>
{_t("Continue")}
</AccessibleButton>
</div>
<input
className='mx_AppPermissionButton'
type='button'
value={_t('Allow')}
onClick={this.props.onPermissionGranted}
/>
</div>
);
}
}
AppPermission.propTypes = {
isRoomEncrypted: PropTypes.bool,
url: PropTypes.string.isRequired,
onPermissionGranted: PropTypes.func.isRequired,
};
AppPermission.defaultProps = {
isRoomEncrypted: false,
onPermissionGranted: function() {},
};

View file

@ -34,7 +34,9 @@ import dis from '../../../dispatcher';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import classNames from 'classnames';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import {createMenu} from "../../structures/ContextualMenu";
import PersistedElement from "./PersistedElement";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false;
@ -52,7 +54,7 @@ export default class AppTile extends React.Component {
this._onLoaded = this._onLoaded.bind(this);
this._onEditClick = this._onEditClick.bind(this);
this._onDeleteClick = this._onDeleteClick.bind(this);
this._onCancelClick = this._onCancelClick.bind(this);
this._onRevokeClicked = this._onRevokeClicked.bind(this);
this._onSnapshotClick = this._onSnapshotClick.bind(this);
this.onClickMenuBar = this.onClickMenuBar.bind(this);
this._onMinimiseClick = this._onMinimiseClick.bind(this);
@ -69,8 +71,11 @@ export default class AppTile extends React.Component {
* @return {Object} Updated component state to be set with setState
*/
_getNewState(newProps) {
const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_');
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
// This is a function to make the impact of calling SettingsStore slightly less
const hasPermissionToLoad = () => {
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", newProps.room.roomId);
return !!currentlyAllowedWidgets[newProps.eventId];
};
const PersistedElement = sdk.getComponent("elements.PersistedElement");
return {
@ -78,10 +83,9 @@ export default class AppTile extends React.Component {
// True while the iframe content is loading
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
widgetUrl: this._addWurlParams(newProps.url),
widgetPermissionId: widgetPermissionId,
// 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: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId,
hasPermissionToLoad: newProps.userId === newProps.creatorUserId || hasPermissionToLoad(),
error: null,
deleting: false,
widgetPageTitle: newProps.widgetPageTitle,
@ -205,7 +209,7 @@ export default class AppTile extends React.Component {
if (!this._scalarClient) {
this._scalarClient = defaultManager.getScalarClient();
}
this._scalarClient.getScalarToken().done((token) => {
this._scalarClient.getScalarToken().then((token) => {
// Append scalar_token as a query param if not already present
this._scalarClient.scalarToken = token;
const u = url.parse(this._addWurlParams(this.props.url));
@ -244,7 +248,8 @@ export default class AppTile extends React.Component {
this.setScalarToken();
}
} else if (nextProps.show && !this.props.show) {
if (this.props.waitForIframeLoad) {
// We assume that persisted widgets are loaded and don't need a spinner.
if (this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey)) {
this.setState({
loading: true,
});
@ -269,7 +274,7 @@ export default class AppTile extends React.Component {
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
}
_onEditClick(e) {
_onEditClick() {
console.log("Edit widget ID ", this.props.id);
if (this.props.onEditClick) {
this.props.onEditClick();
@ -291,7 +296,7 @@ export default class AppTile extends React.Component {
}
}
_onSnapshotClick(e) {
_onSnapshotClick() {
console.warn("Requesting widget snapshot");
ActiveWidgetStore.getWidgetMessaging(this.props.id).getScreenshot()
.catch((err) => {
@ -358,13 +363,9 @@ export default class AppTile extends React.Component {
}
}
_onCancelClick() {
if (this.props.onDeleteClick) {
this.props.onDeleteClick();
} else {
console.log("Revoke widget permissions - %s", this.props.id);
this._revokeWidgetPermission();
}
_onRevokeClicked() {
console.info("Revoke widget permissions - %s", this.props.id);
this._revokeWidgetPermission();
}
/**
@ -446,24 +447,38 @@ export default class AppTile extends React.Component {
});
}
/* TODO -- Store permission in account data so that it is persisted across multiple devices */
_grantWidgetPermission() {
console.warn('Granting permission to load widget - ', this.state.widgetUrl);
localStorage.setItem(this.state.widgetPermissionId, true);
this.setState({hasPermissionToLoad: true});
// Now that we have permission, fetch the IM token
this.setScalarToken();
const roomId = this.props.room.roomId;
console.info("Granting permission for widget to load: " + this.props.eventId);
const current = SettingsStore.getValue("allowedWidgets", roomId);
current[this.props.eventId] = true;
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => {
this.setState({hasPermissionToLoad: true});
// Fetch a token for the integration manager, now that we're allowed to
this.setScalarToken();
}).catch(err => {
console.error(err);
// We don't really need to do anything about this - the user will just hit the button again.
});
}
_revokeWidgetPermission() {
console.warn('Revoking permission to load widget - ', this.state.widgetUrl);
localStorage.removeItem(this.state.widgetPermissionId);
this.setState({hasPermissionToLoad: false});
const roomId = this.props.room.roomId;
console.info("Revoking permission for widget to load: " + this.props.eventId);
const current = SettingsStore.getValue("allowedWidgets", roomId);
current[this.props.eventId] = false;
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => {
this.setState({hasPermissionToLoad: false});
// Force the widget to be non-persistent
ActiveWidgetStore.destroyPersistentWidget(this.props.id);
const PersistedElement = sdk.getComponent("elements.PersistedElement");
PersistedElement.destroyElement(this._persistKey);
// Force the widget to be non-persistent (able to be deleted/forgotten)
ActiveWidgetStore.destroyPersistentWidget(this.props.id);
const PersistedElement = sdk.getComponent("elements.PersistedElement");
PersistedElement.destroyElement(this._persistKey);
}).catch(err => {
console.error(err);
// We don't really need to do anything about this - the user will just hit the button again.
});
}
formatAppTileName() {
@ -528,18 +543,59 @@ export default class AppTile extends React.Component {
}
}
_onPopoutWidgetClick(e) {
_onPopoutWidgetClick() {
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
// window.open(this._getSafeUrl(), '_blank', 'noopener=yes');
Object.assign(document.createElement('a'),
{ target: '_blank', href: this._getSafeUrl(), rel: 'noopener'}).click();
}
_onReloadWidgetClick(e) {
_onReloadWidgetClick() {
// Reload iframe in this way to avoid cross-origin restrictions
this.refs.appFrame.src = this.refs.appFrame.src;
}
_getMenuOptions(ev) {
// TODO: This block of code gets copy/pasted a lot. We should make that happen less.
const menuOptions = {};
const buttonRect = ev.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const buttonLeft = buttonRect.left + window.pageXOffset;
const buttonTop = buttonRect.top + window.pageYOffset;
// Align the right edge of the menu to the left edge of the button
menuOptions.right = window.innerWidth - buttonLeft;
// Align the menu vertically on whichever side of the button has more
// space available.
if (buttonTop < window.innerHeight / 2) {
menuOptions.top = buttonTop;
} else {
menuOptions.bottom = window.innerHeight - buttonTop;
}
return menuOptions;
}
_onContextMenuClick = (ev) => {
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
const menuOptions = {
...this._getMenuOptions(ev),
// A revoke handler is always required
onRevokeClicked: this._onRevokeClicked,
};
const canUserModify = this._canUserModify();
const showEditButton = Boolean(this._scalarClient && canUserModify);
const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
if (showEditButton) menuOptions.onEditClicked = this._onEditClick;
if (showDeleteButton) menuOptions.onDeleteClicked = this._onDeleteClick;
if (showPictureSnapshotButton) menuOptions.onSnapshotClicked = this._onSnapshotClick;
if (this.props.showReload) menuOptions.onReloadClicked = this._onReloadWidgetClick;
createMenu(WidgetContextMenu, menuOptions);
};
render() {
let appTileBody;
@ -549,7 +605,7 @@ export default class AppTile extends React.Component {
}
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
// because that would allow the iframe to prgramatically remove the sandbox attribute, but
// because that would allow the iframe to programmatically remove the sandbox attribute, but
// this would only be for content hosted on the same origin as the riot client: anything
// hosted on the same origin as the client will get the same access as if you clicked
// a link to it.
@ -569,12 +625,14 @@ export default class AppTile extends React.Component {
</div>
);
if (!this.state.hasPermissionToLoad) {
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
appTileBody = (
<div className={appTileBodyClass}>
<AppPermission
isRoomEncrypted={isRoomEncrypted}
roomId={this.props.room.roomId}
creatorUserId={this.props.creatorUserId}
url={this.state.widgetUrl}
isRoomEncrypted={isEncrypted}
onPermissionGranted={this._grantWidgetPermission}
/>
</div>
@ -596,12 +654,7 @@ export default class AppTile extends React.Component {
appTileBody = (
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
{ this.state.loading && loadingElement }
{ /*
The "is" attribute in the following iframe tag is needed in order to enable rendering of the
"allow" attribute, which is unknown to react 15.
*/ }
<iframe
is
allow={iframeFeatures}
ref="appFrame"
src={this._getSafeUrl()}
@ -627,13 +680,6 @@ export default class AppTile extends React.Component {
}
}
// editing is done in scalar
const canUserModify = this._canUserModify();
const showEditButton = Boolean(this._scalarClient && canUserModify);
const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
const showCancelButton = (this.props.showCancel === undefined || this.props.showCancel) && !showDeleteButton;
// Picture snapshot - only show button when apps are maximised.
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
const showMinimiseButton = this.props.showMinimise && this.props.show;
const showMaximiseButton = this.props.showMinimise && !this.props.show;
@ -672,41 +718,17 @@ export default class AppTile extends React.Component {
{ this.props.showTitle && this._getTileTitle() }
</span>
<span className="mx_AppTileMenuBarWidgets">
{ /* Reload widget */ }
{ this.props.showReload && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_reload"
title={_t('Reload widget')}
onClick={this._onReloadWidgetClick}
/> }
{ /* Popout widget */ }
{ this.props.showPopout && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
title={_t('Popout widget')}
onClick={this._onPopoutWidgetClick}
/> }
{ /* Snapshot widget */ }
{ showPictureSnapshotButton && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_snapshot"
title={_t('Picture')}
onClick={this._onSnapshotClick}
/> }
{ /* Edit widget */ }
{ showEditButton && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_edit"
title={_t('Edit')}
onClick={this._onEditClick}
/> }
{ /* Delete widget */ }
{ showDeleteButton && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_delete"
title={_t('Delete widget')}
onClick={this._onDeleteClick}
/> }
{ /* Cancel widget */ }
{ showCancelButton && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_cancel"
title={_t('Revoke widget access')}
onClick={this._onCancelClick}
{ /* Context menu */ }
{ <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
title={_t('More options')}
onClick={this._onContextMenuClick}
/> }
</span>
</div> }
@ -720,6 +742,7 @@ AppTile.displayName ='AppTile';
AppTile.propTypes = {
id: PropTypes.string.isRequired,
eventId: PropTypes.string, // required for room widgets
url: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
room: PropTypes.object.isRequired,

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import Promise from 'bluebird';
/**
* A component which wraps an EditableText, with a spinner while updates take
@ -51,7 +50,7 @@ export default class EditableTextContainer extends React.Component {
this.setState({busy: true});
this.props.getInitialValue().done(
this.props.getInitialValue().then(
(result) => {
if (this._unmounted) { return; }
this.setState({
@ -83,7 +82,7 @@ export default class EditableTextContainer extends React.Component {
errorString: null,
});
this.props.onSubmit(value).done(
this.props.onSubmit(value).then(
() => {
if (this._unmounted) { return; }
this.setState({

View file

@ -54,7 +54,7 @@ export default class ErrorBoundary extends React.PureComponent {
if (!PlatformPeg.get()) return;
MatrixClientPeg.get().stopClient();
MatrixClientPeg.get().store.deleteAllData().done(() => {
MatrixClientPeg.get().store.deleteAllData().then(() => {
PlatformPeg.get().reload();
});
};

View file

@ -0,0 +1,28 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import AccessibleButton from "./AccessibleButton";
export default function FormButton(props) {
const {className, label, kind, ...restProps} = props;
const newClassName = (className || "") + " mx_FormButton";
const allProps = Object.assign({}, restProps,
{className: newClassName, kind: kind || "primary", children: [label]});
return React.createElement(AccessibleButton, allProps);
}
FormButton.propTypes = AccessibleButton.propTypes;

View file

@ -22,7 +22,7 @@ import { _t } from '../../../languageHandler';
const GroupsButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton className="mx_GroupsButton" action="view_my_groups"
<ActionButton className="mx_GroupsButton" action="toggle_my_groups"
label={_t("Communities")}
size={props.size}
tooltip={true}

View file

@ -0,0 +1,34 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import AccessibleButton from "./AccessibleButton";
export default function IconButton(props) {
const {icon, className, ...restProps} = props;
let newClassName = (className || "") + " mx_IconButton";
newClassName = newClassName + " mx_IconButton_icon_" + icon;
const allProps = Object.assign({}, restProps, {className: newClassName});
return React.createElement(AccessibleButton, allProps);
}
IconButton.propTypes = Object.assign({
icon: PropTypes.string,
}, AccessibleButton.propTypes);

View file

@ -84,7 +84,7 @@ export default class ImageView extends React.Component {
title: _t('Error'),
description: _t('You cannot delete this image. (%(code)s)', {code: code}),
});
}).done();
});
},
});
};

View file

@ -49,7 +49,7 @@ export default class LanguageDropdown extends React.Component {
this.setState({langs});
}).catch(() => {
this.setState({langs: ['en']});
}).done();
});
if (!this.props.value) {
// If no value is given, we start with the first

View file

@ -67,13 +67,15 @@ module.exports = createReactClass({
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();
});
const app = WidgetUtils.makeAppConfig(
appEvent.getStateKey(), appEvent.getContent(), appEvent.sender, persistentWidgetInRoomId,
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
persistentWidgetInRoomId, appEvent.getId(),
);
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId);
const AppTile = sdk.getComponent('elements.AppTile');
return <AppTile
key={app.id}
id={app.id}
eventId={app.eventId}
url={app.url}
name={app.name}
type={app.type}

View file

@ -129,10 +129,11 @@ module.exports = createReactClass({
render: function() {
let picker;
const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label;
if (this.state.custom) {
picker = (
<Field id={`powerSelector_custom_${this.props.powerLevelKey}`} type="number"
label={this.props.label || _t("Power level")} max={this.props.maxValue}
label={label} max={this.props.maxValue}
onBlur={this.onCustomBlur} onKeyDown={this.onCustomKeyDown} onChange={this.onCustomChange}
value={String(this.state.customValue)} disabled={this.props.disabled} />
);
@ -151,7 +152,7 @@ module.exports = createReactClass({
picker = (
<Field id={`powerSelector_notCustom_${this.props.powerLevelKey}`} element="select"
label={this.props.label || _t("Power level")} onChange={this.onSelectChange}
label={label} onChange={this.onSelectChange}
value={String(this.state.selectValue)} disabled={this.props.disabled}>
{options}
</Field>

View file

@ -21,7 +21,8 @@ import sdk from '../../../index';
export default class TextWithTooltip extends React.Component {
static propTypes = {
class: PropTypes.string,
tooltip: PropTypes.string.isRequired,
tooltipClass: PropTypes.string,
tooltip: PropTypes.node.isRequired,
};
constructor() {
@ -49,6 +50,7 @@ export default class TextWithTooltip extends React.Component {
<Tooltip
label={this.props.tooltip}
visible={this.state.hover}
tooltipClassName={this.props.tooltipClass}
className={"mx_TextWithTooltip_tooltip"} />
</span>
);

View file

@ -100,7 +100,9 @@ module.exports = createReactClass({
const parent = ReactDOM.findDOMNode(this).parentNode;
let style = {};
style = this._updatePosition(style);
style.display = "block";
// Hide the entire container when not visible. This prevents flashing of the tooltip
// if it is not meant to be visible on first mount.
style.display = this.props.visible ? "block" : "none";
const tooltipClasses = classNames("mx_Tooltip", this.props.tooltipClassName, {
"mx_Tooltip_visible": this.props.visible,

View file

@ -36,7 +36,7 @@ export default createReactClass({
},
componentWillMount: function() {
this.context.matrixClient.getJoinedGroups().done((result) => {
this.context.matrixClient.getJoinedGroups().then((result) => {
this.setState({groups: result.groups || [], error: null});
}, (err) => {
console.error(err);

View file

@ -55,7 +55,7 @@ export default class MAudioBody extends React.Component {
decryptFile(content.file).then(function(blob) {
decryptedBlob = blob;
return URL.createObjectURL(decryptedBlob);
}).done((url) => {
}).then((url) => {
this.setState({
decryptedUrl: url,
decryptedBlob: decryptedBlob,

View file

@ -24,7 +24,6 @@ import MFileBody from './MFileBody';
import Modal from '../../../Modal';
import sdk from '../../../index';
import { decryptFile } from '../../../utils/DecryptFile';
import Promise from 'bluebird';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
@ -289,7 +288,7 @@ export default class MImageBody extends React.Component {
this.setState({
error: err,
});
}).done();
});
}
// Remember that the user wanted to show this particular image

View file

@ -111,10 +111,10 @@ export default class MKeyVerificationRequest extends React.Component {
userLabelForEventRoom(fromUserId, mxEvent)}</div>);
const isResolved = !(this.state.accepted || this.state.cancelled || this.state.done);
if (isResolved) {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const FormButton = sdk.getComponent("elements.FormButton");
stateNode = (<div className="mx_KeyVerification_buttons">
<AccessibleButton kind="decline" onClick={this._onRejectClicked}>{_t("Decline")}</AccessibleButton>
<AccessibleButton kind="accept" onClick={this._onAcceptClicked}>{_t("Accept")}</AccessibleButton>
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
<FormButton onClick={this._onAcceptClicked} label={_t("Accept")} />
</div>);
}
} else if (isOwn) { // request sent by us

View file

@ -20,7 +20,6 @@ import createReactClass from 'create-react-class';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { decryptFile } from '../../../utils/DecryptFile';
import Promise from 'bluebird';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
@ -89,7 +88,7 @@ module.exports = createReactClass({
const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) {
let thumbnailPromise = Promise.resolve(null);
if (content.info.thumbnail_file) {
if (content.info && content.info.thumbnail_file) {
thumbnailPromise = decryptFile(
content.info.thumbnail_file,
).then(function(blob) {
@ -115,7 +114,7 @@ module.exports = createReactClass({
this.setState({
error: err,
});
}).done();
});
}
},

View file

@ -18,6 +18,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import sdk from '../../../index';
import SettingsStore from "../../../settings/SettingsStore";
import {Mjolnir} from "../../../mjolnir/Mjolnir";
module.exports = createReactClass({
displayName: 'MessageEvent',
@ -49,6 +51,10 @@ module.exports = createReactClass({
return this.refs.body && this.refs.body.getEventTileOps ? this.refs.body.getEventTileOps() : null;
},
onTileUpdate: function() {
this.forceUpdate();
},
render: function() {
const UnknownBody = sdk.getComponent('messages.UnknownBody');
@ -81,6 +87,21 @@ module.exports = createReactClass({
}
}
if (SettingsStore.isFeatureEnabled("feature_mjolnir")) {
const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`;
const allowRender = localStorage.getItem(key) === "true";
if (!allowRender) {
const userDomain = this.props.mxEvent.getSender().split(':').slice(1).join(':');
const userBanned = Mjolnir.sharedInstance().isUserBanned(this.props.mxEvent.getSender());
const serverBanned = Mjolnir.sharedInstance().isServerBanned(userDomain);
if (userBanned || serverBanned) {
BodyType = sdk.getComponent('messages.MjolnirBody');
}
}
}
return <BodyType
ref="body" mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
@ -90,6 +111,8 @@ module.exports = createReactClass({
maxImageHeight={this.props.maxImageHeight}
replacingEventId={this.props.replacingEventId}
editState={this.props.editState}
onHeightChanged={this.props.onHeightChanged} />;
onHeightChanged={this.props.onHeightChanged}
onMessageAllowed={this.onTileUpdate}
/>;
},
});

View file

@ -0,0 +1,48 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from '../../../languageHandler';
export default class MjolnirBody extends React.Component {
static propTypes = {
mxEvent: PropTypes.object.isRequired,
onMessageAllowed: PropTypes.func.isRequired,
};
constructor() {
super();
}
_onAllowClick = (e) => {
e.preventDefault();
e.stopPropagation();
const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`;
localStorage.setItem(key, "true");
this.props.onMessageAllowed();
};
render() {
return (
<div className='mx_MjolnirBody'><i>{_t(
"You have ignored this user, so their message is hidden. <a>Show anyways.</a>",
{}, {a: (sub) => <a href="#" onClick={this._onAllowClick}>{sub}</a>},
)}</i></div>
);
}
}

View file

@ -43,7 +43,8 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent {
if (room) {
const senders = [];
for (const reactionEvent of reactionEvents) {
const { name } = room.getMember(reactionEvent.getSender());
const member = room.getMember(reactionEvent.getSender());
const name = member ? member.name : reactionEvent.getSender();
senders.push(name);
}
const shortName = unicodeToShortcode(content);

View file

@ -144,7 +144,7 @@ module.exports = createReactClass({
},
shouldComponentUpdate: function(nextProps, nextState) {
//console.log("shouldComponentUpdate: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
//console.info("shouldComponentUpdate: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
// exploit that events are immutable :)
return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
@ -159,7 +159,7 @@ module.exports = createReactClass({
},
calculateUrlPreview: function() {
//console.log("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
//console.info("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
if (this.props.showUrlPreview) {
let links = this.findLinks(this.refs.content.children);

View file

@ -27,7 +27,6 @@ import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import createRoom from '../../../createRoom';
import DMRoomMap from '../../../utils/DMRoomMap';
import Unread from '../../../Unread';
import AccessibleButton from '../elements/AccessibleButton';
import SdkConfig from '../../../SdkConfig';
import SettingsStore from "../../../settings/SettingsStore";
@ -40,6 +39,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg";
import E2EIcon from "../rooms/E2EIcon";
import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {textualPowerLevel} from '../../../Roles';
const _disambiguateDevices = (devices) => {
const names = Object.create(null);
@ -63,10 +63,92 @@ const _getE2EStatus = (devices) => {
return hasUnverifiedDevice ? "warning" : "verified";
};
const DevicesSection = ({devices, userId, loading}) => {
const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo');
async function unverifyUser(matrixClient, userId) {
const devices = await matrixClient.getStoredDevicesForUser(userId);
for (const device of devices) {
if (device.isVerified()) {
matrixClient.setDeviceVerified(
userId, device.deviceId, false,
);
}
}
}
function openDMForUser(matrixClient, userId) {
const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId);
const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => {
const room = matrixClient.getRoom(roomId);
if (!room || room.getMyMembership() === "leave") {
return lastActiveRoom;
}
if (!lastActiveRoom || lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp()) {
return room;
}
return lastActiveRoom;
}, null);
if (lastActiveRoom) {
dis.dispatch({
action: 'view_room',
room_id: lastActiveRoom.roomId,
});
} else {
createRoom({dmUserId: userId});
}
}
function useIsEncrypted(cli, room) {
const [isEncrypted, setIsEncrypted] = useState(cli.isRoomEncrypted(room.roomId));
const update = useCallback((event) => {
if (event.getType() === "m.room.encryption") {
setIsEncrypted(cli.isRoomEncrypted(room.roomId));
}
}, [cli, room]);
useEventEmitter(room.currentState, "RoomState.events", update);
return isEncrypted;
}
function verifyDevice(userId, device) {
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
userId: userId,
device: device,
});
}
function DeviceItem({userId, device}) {
const classes = classNames("mx_UserInfo_device", {
mx_UserInfo_device_verified: device.isVerified(),
mx_UserInfo_device_unverified: !device.isVerified(),
});
const iconClasses = classNames("mx_E2EIcon", {
mx_E2EIcon_verified: device.isVerified(),
mx_E2EIcon_warning: !device.isVerified(),
});
const onDeviceClick = () => {
if (!device.isVerified()) {
verifyDevice(userId, device);
}
};
const deviceName = device.ambiguous ?
(device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" :
device.getDisplayName();
const trustedLabel = device.isVerified() ? _t("Trusted") : _t("Not trusted");
return (<AccessibleButton className={classes} onClick={onDeviceClick}>
<div className={iconClasses} />
<div className="mx_UserInfo_device_name">{deviceName}</div>
<div className="mx_UserInfo_device_trusted">{trustedLabel}</div>
</AccessibleButton>);
}
function DevicesSection({devices, userId, loading}) {
const Spinner = sdk.getComponent("elements.Spinner");
const [isExpanded, setExpanded] = useState(false);
if (loading) {
// still loading
return <Spinner />;
@ -74,123 +156,50 @@ const DevicesSection = ({devices, userId, loading}) => {
if (devices === null) {
return _t("Unable to load device list");
}
if (devices.length === 0) {
return _t("No devices with registered encryption keys");
}
return (
<div className="mx_UserInfo_container">
<h3>{ _t("Trust & Devices") }</h3>
<div className="mx_UserInfo_devices">
{ devices.map((device, i) => <MemberDeviceInfo key={i} userId={userId} device={device} />) }
</div>
</div>
);
};
const unverifiedDevices = devices.filter(d => !d.isVerified());
const verifiedDevices = devices.filter(d => d.isVerified());
const onRoomTileClick = (roomId) => {
dis.dispatch({
action: 'view_room',
room_id: roomId,
});
};
const DirectChatsSection = withLegacyMatrixClient(({matrixClient: cli, userId, startUpdating, stopUpdating}) => {
const onNewDMClick = async () => {
startUpdating();
await createRoom({dmUserId: userId});
stopUpdating();
};
// TODO: Immutable DMs replaces a lot of this
// dmRooms will not include dmRooms that we have been invited into but did not join.
// Because DMRoomMap runs off account_data[m.direct] which is only set on join of dm room.
// XXX: we potentially want DMs we have been invited to, to also show up here :L
// especially as logic below concerns specially if we haven't joined but have been invited
const [dmRooms, setDmRooms] = useState(new DMRoomMap(cli).getDMRoomsForUserId(userId));
// TODO bind the below
// cli.on("Room", this.onRoom);
// cli.on("Room.name", this.onRoomName);
// cli.on("deleteRoom", this.onDeleteRoom);
const accountDataHandler = useCallback((ev) => {
if (ev.getType() === "m.direct") {
const dmRoomMap = new DMRoomMap(cli);
setDmRooms(dmRoomMap.getDMRoomsForUserId(userId));
}
}, [cli, userId]);
useEventEmitter(cli, "accountData", accountDataHandler);
const RoomTile = sdk.getComponent("rooms.RoomTile");
const tiles = [];
for (const roomId of dmRooms) {
const room = cli.getRoom(roomId);
if (room) {
const myMembership = room.getMyMembership();
// not a DM room if we have are not joined
if (myMembership !== 'join') continue;
const them = room.getMember(userId);
// not a DM room if they are not joined
if (!them || !them.membership || them.membership !== 'join') continue;
const highlight = room.getUnreadNotificationCount('highlight') > 0;
tiles.push(
<RoomTile key={room.roomId}
room={room}
transparent={true}
collapsed={false}
selected={false}
unread={Unread.doesRoomHaveUnreadMessages(room)}
highlight={highlight}
isInvite={false}
onClick={onRoomTileClick}
/>,
);
let expandButton;
if (verifiedDevices.length) {
if (isExpanded) {
expandButton = (<AccessibleButton className="mx_UserInfo_expand" onClick={() => setExpanded(false)}>
<div>{_t("Hide verified Sign-In's")}</div>
</AccessibleButton>);
} else {
expandButton = (<AccessibleButton className="mx_UserInfo_expand" onClick={() => setExpanded(true)}>
<div className="mx_E2EIcon mx_E2EIcon_verified" />
<div>{_t("%(count)s verified Sign-In's", {count: verifiedDevices.length})}</div>
</AccessibleButton>);
}
}
const labelClasses = classNames({
mx_UserInfo_createRoom_label: true,
mx_RoomTile_name: true,
let deviceList = unverifiedDevices.map((device, i) => {
return (<DeviceItem key={i} userId={userId} device={device} />);
});
let body = tiles;
if (!body) {
body = (
<AccessibleButton className="mx_UserInfo_createRoom" onClick={onNewDMClick}>
<div className="mx_RoomTile_avatar">
<img src={require("../../../../res/img/create-big.svg")} width="26" height="26" alt={_t("Start a chat")} />
</div>
<div className={labelClasses}><i>{ _t("Start a chat") }</i></div>
</AccessibleButton>
);
if (isExpanded) {
const keyStart = unverifiedDevices.length;
deviceList = deviceList.concat(verifiedDevices.map((device, i) => {
return (<DeviceItem key={i + keyStart} userId={userId} device={device} />);
}));
}
return (
<div className="mx_UserInfo_container">
<div className="mx_UserInfo_container_header">
<h3>{ _t("Direct messages") }</h3>
<AccessibleButton
className="mx_UserInfo_container_header_right mx_UserInfo_newDmButton"
onClick={onNewDMClick}
title={_t("Start a chat")}
/>
</div>
{ body }
<div className="mx_UserInfo_devices">
<div>{deviceList}</div>
<div>{expandButton}</div>
</div>
);
});
}
const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite}) => {
const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite, devices}) => {
let ignoreButton = null;
let insertPillButton = null;
let inviteUserButton = null;
let readReceiptButton = null;
const isMe = member.userId === cli.getUserId();
const onShareUserClick = () => {
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
Modal.createTrackedDialog('share room member dialog', '', ShareDialog, {
@ -200,7 +209,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
// Only allow the user to ignore the user if its not ourselves
// same goes for jumping to read receipt
if (member.userId !== cli.getUserId()) {
if (!isMe) {
const onIgnoreToggle = () => {
const ignoredUsers = cli.getIgnoredUsers();
if (isIgnored) {
@ -214,7 +223,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
};
ignoreButton = (
<AccessibleButton onClick={onIgnoreToggle} className="mx_UserInfo_field">
<AccessibleButton onClick={onIgnoreToggle} className={classNames("mx_UserInfo_field", {mx_UserInfo_destructive: !isIgnored})}>
{ isIgnored ? _t("Unignore") : _t("Ignore") }
</AccessibleButton>
);
@ -285,15 +294,34 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
</AccessibleButton>
);
let directMessageButton;
if (!isMe) {
directMessageButton = (
<AccessibleButton onClick={() => openDMForUser(cli, member.userId)} className="mx_UserInfo_field">
{ _t('Direct message') }
</AccessibleButton>
);
}
let unverifyButton;
if (devices && devices.some(device => device.isVerified())) {
unverifyButton = (
<AccessibleButton onClick={() => unverifyUser(cli, member.userId)} className="mx_UserInfo_field mx_UserInfo_destructive">
{ _t('Unverify user') }
</AccessibleButton>
);
}
return (
<div className="mx_UserInfo_container">
<h3>{ _t("User Options") }</h3>
<div className="mx_UserInfo_buttons">
<h3>{ _t("Options") }</h3>
<div>
{ directMessageButton }
{ readReceiptButton }
{ shareUserButton }
{ insertPillButton }
{ ignoreButton }
{ inviteUserButton }
{ ignoreButton }
{ unverifyButton }
</div>
</div>
);
@ -337,10 +365,13 @@ const _isMuted = (member, powerLevelContent) => {
return member.powerLevel < levelToSend;
};
const useRoomPowerLevels = (room) => {
const useRoomPowerLevels = (cli, room) => {
const [powerLevels, setPowerLevels] = useState({});
const update = useCallback(() => {
if (!room) {
return;
}
const event = room.currentState.getStateEvents("m.room.power_levels", "");
if (event) {
setPowerLevels(event.getContent());
@ -352,7 +383,7 @@ const useRoomPowerLevels = (room) => {
};
}, [room]);
useEventEmitter(room, "RoomState.events", update);
useEventEmitter(cli, "RoomState.members", update);
useEffect(() => {
update();
return () => {
@ -399,7 +430,7 @@ const RoomKickButton = withLegacyMatrixClient(({matrixClient: cli, member, start
};
const kickLabel = member.membership === "invite" ? _t("Disinvite") : _t("Kick");
return <AccessibleButton className="mx_UserInfo_field" onClick={onKick}>
return <AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={onKick}>
{ kickLabel }
</AccessibleButton>;
});
@ -472,7 +503,7 @@ const RedactMessagesButton = withLegacyMatrixClient(({matrixClient: cli, member}
}
};
return <AccessibleButton className="mx_UserInfo_field" onClick={onRedactAllMessages}>
return <AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={onRedactAllMessages}>
{ _t("Remove recent messages") }
</AccessibleButton>;
});
@ -524,7 +555,11 @@ const BanToggleButton = withLegacyMatrixClient(({matrixClient: cli, member, star
label = _t("Unban");
}
return <AccessibleButton className="mx_UserInfo_field" onClick={onBanOrUnban}>
const classes = classNames("mx_UserInfo_field", {
mx_UserInfo_destructive: member.membership !== 'ban',
});
return <AccessibleButton className={classes} onClick={onBanOrUnban}>
{ label }
</AccessibleButton>;
});
@ -581,21 +616,24 @@ const MuteToggleButton = withLegacyMatrixClient(
}
};
const classes = classNames("mx_UserInfo_field", {
mx_UserInfo_destructive: !isMuted,
});
const muteLabel = isMuted ? _t("Unmute") : _t("Mute");
return <AccessibleButton className="mx_UserInfo_field" onClick={onMuteToggle}>
return <AccessibleButton className={classes} onClick={onMuteToggle}>
{ muteLabel }
</AccessibleButton>;
},
);
const RoomAdminToolsContainer = withLegacyMatrixClient(
({matrixClient: cli, room, children, member, startUpdating, stopUpdating}) => {
({matrixClient: cli, room, children, member, startUpdating, stopUpdating, powerLevels}) => {
let kickButton;
let banButton;
let muteButton;
let redactButton;
const powerLevels = useRoomPowerLevels(room);
const editPowerLevel = (
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
powerLevels.state_default
@ -705,7 +743,7 @@ const GroupAdminToolsSection = withLegacyMatrixClient(
};
const kickButton = (
<AccessibleButton className="mx_UserInfo_field" onClick={_onKick}>
<AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={_onKick}>
{ isInvited ? _t('Disinvite') : _t('Remove from community') }
</AccessibleButton>
);
@ -744,47 +782,17 @@ const useIsSynapseAdmin = (cli) => {
return isAdmin;
};
// cli is injected by withLegacyMatrixClient
const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, roomId, onClose}) => {
// Load room if we are given a room id and memoize it
const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]);
// only display the devices list if our client supports E2E
const _enableDevices = cli.isCryptoEnabled();
// Load whether or not we are a Synapse Admin
const isSynapseAdmin = useIsSynapseAdmin(cli);
// Check whether the user is ignored
const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(user.userId));
// Recheck if the user or client changes
useEffect(() => {
setIsIgnored(cli.isUserIgnored(user.userId));
}, [cli, user.userId]);
// Recheck also if we receive new accountData m.ignored_user_list
const accountDataHandler = useCallback((ev) => {
if (ev.getType() === "m.ignored_user_list") {
setIsIgnored(cli.isUserIgnored(user.userId));
}
}, [cli, user.userId]);
useEventEmitter(cli, "accountData", accountDataHandler);
// Count of how many operations are currently in progress, if > 0 then show a Spinner
const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
const startUpdating = useCallback(() => {
setPendingUpdateCount(pendingUpdateCount + 1);
}, [pendingUpdateCount]);
const stopUpdating = useCallback(() => {
setPendingUpdateCount(pendingUpdateCount - 1);
}, [pendingUpdateCount]);
function useRoomPermissions(cli, room, user) {
const [roomPermissions, setRoomPermissions] = useState({
// modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
modifyLevelMax: -1,
canEdit: false,
canInvite: false,
});
const updateRoomPermissions = useCallback(async () => {
if (!room) return;
const updateRoomPermissions = useCallback(() => {
if (!room) {
return;
}
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (!powerLevelEvent) return;
@ -811,20 +819,197 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
setRoomPermissions({
canInvite: me.powerLevel >= powerLevels.invite,
canEdit: modifyLevelMax >= 0,
modifyLevelMax,
});
}, [cli, user, room]);
useEventEmitter(cli, "RoomState.events", updateRoomPermissions);
useEventEmitter(cli, "RoomState.members", updateRoomPermissions);
useEffect(() => {
updateRoomPermissions();
return () => {
setRoomPermissions({
maximalPowerLevel: -1,
canEdit: false,
canInvite: false,
});
};
}, [updateRoomPermissions]);
return roomPermissions;
}
const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, powerLevels}) => {
const [isEditing, setEditing] = useState(false);
if (room && user.roomId) { // is in room
if (isEditing) {
return (<PowerLevelEditor
user={user} room={room} roomPermissions={roomPermissions}
onFinished={() => setEditing(false)} />);
} else {
const IconButton = sdk.getComponent('elements.IconButton');
const powerLevelUsersDefault = powerLevels.users_default || 0;
const powerLevel = parseInt(user.powerLevel, 10);
const modifyButton = roomPermissions.canEdit ?
(<IconButton icon="edit" onClick={() => setEditing(true)} />) : null;
const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
const label = _t("<strong>%(role)s</strong> in %(roomName)s",
{role, roomName: room.name},
{strong: label => <strong>{label}</strong>},
);
return (
<div className="mx_UserInfo_profileField">
<div className="mx_UserInfo_roleDescription">{label}{modifyButton}</div>
</div>
);
}
} else {
return null;
}
});
const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, onFinished}) => {
const [isUpdating, setIsUpdating] = useState(false);
const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10));
const [isDirty, setIsDirty] = useState(false);
const onPowerChange = useCallback((powerLevel) => {
setIsDirty(true);
setSelectedPowerLevel(parseInt(powerLevel, 10));
}, [setSelectedPowerLevel, setIsDirty]);
const changePowerLevel = useCallback(async () => {
const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
return cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
function() {
// NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here!
console.log("Power change success");
}, function(err) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to change power level " + err);
Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, {
title: _t("Error"),
description: _t("Failed to change power level"),
});
},
);
};
try {
if (!isDirty) {
return;
}
setIsUpdating(true);
const powerLevel = selectedPowerLevel;
const roomId = user.roomId;
const target = user.userId;
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (!powerLevelEvent) return;
if (!powerLevelEvent.getContent().users) {
_applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
return;
}
const myUserId = cli.getUserId();
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
if (myUserId === target) {
try {
if (!(await _warnSelfDemote())) return;
} catch (e) {
console.error("Failed to warn about self demotion: ", e);
}
await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
return;
}
const myPower = powerLevelEvent.getContent().users[myUserId];
if (parseInt(myPower) === parseInt(powerLevel)) {
const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, {
title: _t("Warning!"),
description:
<div>
{ _t("You will not be able to undo this change as you are promoting the user " +
"to have the same power level as yourself.") }<br />
{ _t("Are you sure?") }
</div>,
button: _t("Continue"),
});
const [confirmed] = await finished;
if (confirmed) return;
}
await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
} finally {
onFinished();
}
}, [user.roomId, user.userId, cli, selectedPowerLevel, isDirty, setIsUpdating, onFinished, room]);
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
const IconButton = sdk.getComponent('elements.IconButton');
const Spinner = sdk.getComponent("elements.Spinner");
const buttonOrSpinner = isUpdating ? <Spinner w={16} h={16} /> :
<IconButton icon="check" onClick={changePowerLevel} />;
const PowerSelector = sdk.getComponent('elements.PowerSelector');
return (
<div className="mx_UserInfo_profileField">
<PowerSelector
label={null}
value={selectedPowerLevel}
maxValue={roomPermissions.modifyLevelMax}
usersDefault={powerLevelUsersDefault}
onChange={onPowerChange}
disabled={isUpdating}
/>
{buttonOrSpinner}
</div>
);
});
// cli is injected by withLegacyMatrixClient
const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, roomId, onClose}) => {
// Load room if we are given a room id and memoize it
const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]);
// only display the devices list if our client supports E2E
const _enableDevices = cli.isCryptoEnabled();
const powerLevels = useRoomPowerLevels(cli, room);
// Load whether or not we are a Synapse Admin
const isSynapseAdmin = useIsSynapseAdmin(cli);
// Check whether the user is ignored
const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(user.userId));
// Recheck if the user or client changes
useEffect(() => {
setIsIgnored(cli.isUserIgnored(user.userId));
}, [cli, user.userId]);
// Recheck also if we receive new accountData m.ignored_user_list
const accountDataHandler = useCallback((ev) => {
if (ev.getType() === "m.ignored_user_list") {
setIsIgnored(cli.isUserIgnored(user.userId));
}
}, [cli, user.userId]);
useEventEmitter(cli, "accountData", accountDataHandler);
// Count of how many operations are currently in progress, if > 0 then show a Spinner
const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
const startUpdating = useCallback(() => {
setPendingUpdateCount(pendingUpdateCount + 1);
}, [pendingUpdateCount]);
const stopUpdating = useCallback(() => {
setPendingUpdateCount(pendingUpdateCount - 1);
}, [pendingUpdateCount]);
const roomPermissions = useRoomPermissions(cli, room, user);
const onSynapseDeactivate = useCallback(async () => {
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, {
@ -842,80 +1027,25 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
const [accepted] = await finished;
if (!accepted) return;
try {
cli.deactivateSynapseUser(user.userId);
await cli.deactivateSynapseUser(user.userId);
} catch (err) {
console.error("Failed to deactivate user");
console.error(err);
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('Failed to deactivate user', '', ErrorDialog, {
Modal.createTrackedDialog('Failed to deactivate Synapse user', '', ErrorDialog, {
title: _t('Failed to deactivate user'),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
}
}, [cli, user.userId]);
const onPowerChange = useCallback(async (powerLevel) => {
const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
startUpdating();
cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
function() {
// NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here!
console.log("Power change success");
}, function(err) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to change power level " + err);
Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, {
title: _t("Error"),
description: _t("Failed to change power level"),
});
},
).finally(() => {
stopUpdating();
}).done();
};
const roomId = user.roomId;
const target = user.userId;
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (!powerLevelEvent) return;
if (!powerLevelEvent.getContent().users) {
_applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
return;
const onMemberAvatarKey = e => {
if (e.key === "Enter") {
onMemberAvatarClick();
}
const myUserId = cli.getUserId();
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
if (myUserId === target) {
try {
if (!(await _warnSelfDemote())) return;
_applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
} catch (e) {
console.error("Failed to warn about self demotion: ", e);
}
return;
}
const myPower = powerLevelEvent.getContent().users[myUserId];
if (parseInt(myPower) === parseInt(powerLevel)) {
const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, {
title: _t("Warning!"),
description:
<div>
{ _t("You will not be able to undo this change as you are promoting the user " +
"to have the same power level as yourself.") }<br />
{ _t("Are you sure?") }
</div>,
button: _t("Continue"),
});
const [confirmed] = await finished;
if (confirmed) return;
}
_applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
}, [user.roomId, user.userId, room && room.currentState, cli]); // eslint-disable-line
};
const onMemberAvatarClick = useCallback(() => {
const member = user;
@ -935,17 +1065,12 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
let synapseDeactivateButton;
let spinner;
let directChatsSection;
if (user.userId !== cli.getUserId()) {
directChatsSection = <DirectChatsSection userId={user.userId} />;
}
// We don't need a perfect check here, just something to pass as "probably not our homeserver". If
// someone does figure out how to bypass this check the worst that happens is an error.
// FIXME this should be using cli instead of MatrixClientPeg.matrixClient
if (isSynapseAdmin && user.userId.endsWith(`:${MatrixClientPeg.getHomeserverName()}`)) {
synapseDeactivateButton = (
<AccessibleButton onClick={onSynapseDeactivate} className="mx_UserInfo_field">
<AccessibleButton onClick={onSynapseDeactivate} className="mx_UserInfo_field mx_UserInfo_destructive">
{_t("Deactivate user")}
</AccessibleButton>
);
@ -955,6 +1080,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
if (room && user.roomId) {
adminToolsContainer = (
<RoomAdminToolsContainer
powerLevels={powerLevels}
member={user}
room={room}
startUpdating={startUpdating}
@ -992,7 +1118,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
let presenceCurrentlyActive;
let statusMessage;
if (user instanceof RoomMember) {
if (user instanceof RoomMember && user.user) {
presenceState = user.user.presence;
presenceLastActiveAgo = user.user.lastActiveAgo;
presenceCurrentlyActive = user.user.currentlyActive;
@ -1021,32 +1147,19 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
statusLabel = <span className="mx_UserInfo_statusMessage">{ statusMessage }</span>;
}
let memberDetails = null;
if (room && user.roomId) { // is in room
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
const PowerSelector = sdk.getComponent('elements.PowerSelector');
memberDetails = <div>
<div className="mx_UserInfo_profileField">
<PowerSelector
value={parseInt(user.powerLevel)}
maxValue={roomPermissions.modifyLevelMax}
disabled={roomPermissions.modifyLevelMax < 0}
usersDefault={powerLevelUsersDefault}
onChange={onPowerChange} />
</div>
</div>;
}
const avatarUrl = user.getMxcAvatarUrl ? user.getMxcAvatarUrl() : user.avatarUrl;
let avatarElement;
if (avatarUrl) {
const httpUrl = cli.mxcUrlToHttp(avatarUrl, 800, 800);
avatarElement = <div className="mx_UserInfo_avatar" onClick={onMemberAvatarClick}>
<img src={httpUrl} alt={_t("Profile picture")} />
avatarElement = <div
className="mx_UserInfo_avatar"
onClick={onMemberAvatarClick}
onKeyDown={onMemberAvatarKey}
tabIndex="0"
role="img"
aria-label={_t("Profile picture")}
>
<div><div style={{backgroundImage: `url(${httpUrl})`}} /></div>
</div>;
}
@ -1058,6 +1171,12 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
title={_t('Close')} />;
}
const memberDetails = <PowerLevelSection
powerLevels={powerLevels}
user={user} room={room} roomPermissions={roomPermissions}
/>;
const isRoomEncrypted = useIsEncrypted(cli, room);
// undefined means yet to be loaded, null means failed to load, otherwise list of devices
const [devices, setDevices] = useState(undefined);
// Download device lists
@ -1082,14 +1201,15 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
setDevices(null);
}
}
_downloadDeviceList();
if (isRoomEncrypted) {
_downloadDeviceList();
}
// Handle being unmounted
return () => {
cancelled = true;
};
}, [cli, user.userId]);
}, [cli, user.userId, isRoomEncrypted]);
// Listen to changes
useEffect(() => {
@ -1106,21 +1226,20 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
}
};
cli.on("deviceVerificationChanged", onDeviceVerificationChanged);
if (isRoomEncrypted) {
cli.on("deviceVerificationChanged", onDeviceVerificationChanged);
}
// Handle being unmounted
return () => {
cancel = true;
cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
if (isRoomEncrypted) {
cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
}
};
}, [cli, user.userId]);
let devicesSection;
const isRoomEncrypted = _enableDevices && room && cli.isRoomEncrypted(room.roomId);
if (isRoomEncrypted) {
devicesSection = <DevicesSection loading={devices === undefined} devices={devices} userId={user.userId} />;
} else {
let text;
}, [cli, user.userId, isRoomEncrypted]);
let text;
if (!isRoomEncrypted) {
if (!_enableDevices) {
text = _t("This client does not support end-to-end encryption.");
} else if (room) {
@ -1128,22 +1247,24 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
} else {
// TODO what to render for GroupMember
}
if (text) {
devicesSection = (
<div className="mx_UserInfo_container">
<h3>{ _t("Trust & Devices") }</h3>
<div className="mx_UserInfo_devices">
{ text }
</div>
</div>
);
}
} else {
text = _t("Messages in this room are end-to-end encrypted.");
}
const devicesSection = isRoomEncrypted ?
(<DevicesSection loading={devices === undefined} devices={devices} userId={user.userId} />) : null;
const securitySection = (
<div className="mx_UserInfo_container">
<h3>{ _t("Security") }</h3>
<p>{ text }</p>
<AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyDevice(user.userId, null)}>{_t("Verify")}</AccessibleButton>
{ devicesSection }
</div>
);
let e2eIcon;
if (isRoomEncrypted && devices) {
e2eIcon = <E2EIcon status={_getE2EStatus(devices)} isUser={true} />;
e2eIcon = <E2EIcon size={18} status={_getE2EStatus(devices)} isUser={true} />;
}
return (
@ -1153,16 +1274,14 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
<div className="mx_UserInfo_container">
<div className="mx_UserInfo_profile">
<div className="mx_UserInfo_profileField">
<h2>
<div >
<h2 aria-label={displayName}>
{ e2eIcon }
{ displayName }
</h2>
</div>
<div className="mx_UserInfo_profileField">
{ user.userId }
</div>
<div className="mx_UserInfo_profileField">
<div>{ user.userId }</div>
<div className="mx_UserInfo_profileStatus">
{presenceLabel}
{statusLabel}
</div>
@ -1176,11 +1295,9 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
</div> }
<AutoHideScrollbar className="mx_UserInfo_scrollContainer">
{ devicesSection }
{ directChatsSection }
{ securitySection }
<UserOptionsSection
devices={devices}
canInvite={roomPermissions.canInvite}
isIgnored={isIgnored}
member={user} />

View file

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';

View file

@ -107,7 +107,9 @@ module.exports = createReactClass({
this.props.room.roomId, WidgetUtils.getRoomWidgets(this.props.room),
);
return widgets.map((ev) => {
return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.sender);
return WidgetUtils.makeAppConfig(
ev.getStateKey(), ev.getContent(), ev.getSender(), ev.getRoomId(), ev.getId(),
);
});
},
@ -159,6 +161,7 @@ module.exports = createReactClass({
return (<AppTile
key={app.id}
id={app.id}
eventId={app.eventId}
url={app.url}
name={app.name}
type={app.type}

View file

@ -21,11 +21,11 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import flatMap from 'lodash/flatMap';
import type {Completion} from '../../../autocomplete/Autocompleter';
import Promise from 'bluebird';
import { Room } from 'matrix-js-sdk';
import SettingsStore from "../../../settings/SettingsStore";
import Autocompleter from '../../../autocomplete/Autocompleter';
import {sleep} from "../../../utils/promise";
const COMPOSER_SELECTED = 0;
@ -105,13 +105,11 @@ export default class Autocomplete extends React.Component {
autocompleteDelay = 0;
}
const deferred = Promise.defer();
this.debounceCompletionsRequest = setTimeout(() => {
this.processQuery(query, selection).then(() => {
deferred.resolve();
});
}, autocompleteDelay);
return deferred.promise;
return new Promise((resolve) => {
this.debounceCompletionsRequest = setTimeout(() => {
resolve(this.processQuery(query, selection));
}, autocompleteDelay);
});
}
processQuery(query, selection) {
@ -197,16 +195,16 @@ export default class Autocomplete extends React.Component {
}
forceComplete() {
const done = Promise.defer();
this.setState({
forceComplete: true,
hide: false,
}, () => {
this.complete(this.props.query, this.props.selection).then(() => {
done.resolve(this.countCompletions());
return new Promise((resolve) => {
this.setState({
forceComplete: true,
hide: false,
}, () => {
this.complete(this.props.query, this.props.selection).then(() => {
resolve(this.countCompletions());
});
});
});
return done.promise;
}
onCompletionClicked(selectionOffset: number): boolean {

View file

@ -36,7 +36,13 @@ export default function(props) {
_t("All devices for this user are trusted") :
_t("All devices in this encrypted room are trusted");
}
const icon = (<div className={e2eIconClasses} title={e2eTitle} />);
let style = null;
if (props.size) {
style = {width: `${props.size}px`, height: `${props.size}px`};
}
const icon = (<div className={e2eIconClasses} style={style} title={e2eTitle} />);
if (props.onClick) {
return (<AccessibleButton onClick={props.onClick}>{ icon }</AccessibleButton>);
} else {

View file

@ -548,7 +548,7 @@ module.exports = createReactClass({
const SenderProfile = sdk.getComponent('messages.SenderProfile');
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
//console.log("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
//console.info("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
const content = this.props.mxEvent.getContent();
const msgtype = content.msgtype;
@ -606,8 +606,8 @@ module.exports = createReactClass({
mx_EventTile_last: this.props.last,
mx_EventTile_contextual: this.props.contextual,
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
mx_EventTile_verified: this.state.verified === true,
mx_EventTile_unverified: this.state.verified === false,
mx_EventTile_verified: !isBubbleMessage && this.state.verified === true,
mx_EventTile_unverified: !isBubbleMessage && this.state.verified === false,
mx_EventTile_bad: isEncryptionFailure,
mx_EventTile_emote: msgtype === 'm.emote',
mx_EventTile_redacted: isRedacted,
@ -800,7 +800,7 @@ module.exports = createReactClass({
<a href={permalink} onClick={this.onPermalinkClicked}>
{ timestamp }
</a>
{ this._renderE2EPadlock() }
{ !isBubbleMessage && this._renderE2EPadlock() }
{ thread }
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
@ -826,7 +826,7 @@ module.exports = createReactClass({
{ readAvatars }
</div>
{ sender }
<div className={classNames("mx_EventTile_line", {mx_EventTile_bubbleLine: isBubbleMessage})}>
<div className="mx_EventTile_line">
<a
href={permalink}
onClick={this.onPermalinkClicked}
@ -834,7 +834,7 @@ module.exports = createReactClass({
>
{ timestamp }
</a>
{ this._renderE2EPadlock() }
{ !isBubbleMessage && this._renderE2EPadlock() }
{ thread }
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}

View file

@ -53,7 +53,7 @@ module.exports = createReactClass({
);
}, (error)=>{
console.error("Failed to get URL preview: " + error);
}).done();
});
},
componentDidMount: function() {

View file

@ -248,7 +248,7 @@ module.exports = createReactClass({
return client.getStoredDevicesForUser(member.userId);
}).finally(function() {
self._cancelDeviceList = null;
}).done(function(devices) {
}).then(function(devices) {
if (cancelled) {
// we got cancelled - presumably a different user now
return;
@ -550,7 +550,16 @@ module.exports = createReactClass({
danger: true,
onFinished: (accepted) => {
if (!accepted) return;
this.context.matrixClient.deactivateSynapseUser(this.props.member.userId);
this.context.matrixClient.deactivateSynapseUser(this.props.member.userId).catch(e => {
console.error("Failed to deactivate user");
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to deactivate Synapse user', '', ErrorDialog, {
title: _t('Failed to deactivate user'),
description: ((e && e.message) ? e.message : _t("Operation failed")),
});
});
},
});
},
@ -572,7 +581,7 @@ module.exports = createReactClass({
},
).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
}).done();
});
},
onPowerChange: async function(powerLevel) {
@ -629,7 +638,7 @@ module.exports = createReactClass({
this.setState({ updating: this.state.updating + 1 });
createRoom({dmUserId: this.props.member.userId}).finally(() => {
this.setState({ updating: this.state.updating - 1 });
}).done();
});
},
onLeaveClick: function() {
@ -689,7 +698,7 @@ module.exports = createReactClass({
const canAffectUser = them.powerLevel < me.powerLevel || isMe;
if (!canAffectUser) {
//console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
//console.info("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
return can;
}
const editPowerLevel = (

View file

@ -25,7 +25,6 @@ import RoomViewStore from '../../../stores/RoomViewStore';
import Stickerpicker from './Stickerpicker';
import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
import ContentMessages from '../../../ContentMessages';
import classNames from 'classnames';
import E2EIcon from './E2EIcon';
function ComposerAvatar(props) {
@ -353,13 +352,9 @@ export default class MessageComposer extends React.Component {
);
}
const wrapperClasses = classNames({
mx_MessageComposer_wrapper: true,
mx_MessageComposer_hasE2EIcon: !!this.props.e2eStatus,
});
return (
<div className="mx_MessageComposer">
<div className={wrapperClasses}>
<div className="mx_MessageComposer_wrapper">
<div className="mx_MessageComposer_row">
{ controls }
</div>

View file

@ -31,6 +31,9 @@ import {_t} from "../../../languageHandler";
const MAX_ROOMS = 20;
const MIN_ROOMS_BEFORE_ENABLED = 10;
// The threshold time in milliseconds to wait for an autojoined room to show up.
const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90 seconds
export default class RoomBreadcrumbs extends React.Component {
constructor(props) {
super(props);
@ -38,6 +41,10 @@ export default class RoomBreadcrumbs extends React.Component {
this.onAction = this.onAction.bind(this);
this._dispatcherRef = null;
// The room IDs we're waiting to come down the Room handler and when we
// started waiting for them. Used to track a room over an upgrade/autojoin.
this._waitingRoomQueue = [/* { roomId, addedTs } */];
}
componentWillMount() {
@ -54,7 +61,7 @@ export default class RoomBreadcrumbs extends React.Component {
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
MatrixClientPeg.get().on("Room", this.onRoomMembershipChanged);
MatrixClientPeg.get().on("Room", this.onRoom);
}
componentWillUnmount() {
@ -68,7 +75,7 @@ export default class RoomBreadcrumbs extends React.Component {
client.removeListener("Room.receipt", this.onRoomReceipt);
client.removeListener("Room.timeline", this.onRoomTimeline);
client.removeListener("Event.decrypted", this.onEventDecrypted);
client.removeListener("Room", this.onRoomMembershipChanged);
client.removeListener("Room", this.onRoom);
}
}
@ -87,6 +94,12 @@ export default class RoomBreadcrumbs extends React.Component {
onAction(payload) {
switch (payload.action) {
case 'view_room':
if (payload.auto_join && !MatrixClientPeg.get().getRoom(payload.room_id)) {
// Queue the room instead of pushing it immediately - we're probably just waiting
// for a join to complete (ie: joining the upgraded room).
this._waitingRoomQueue.push({roomId: payload.room_id, addedTs: (new Date).getTime()});
break;
}
this._appendRoomId(payload.room_id);
break;
@ -153,7 +166,20 @@ export default class RoomBreadcrumbs extends React.Component {
if (!this.state.enabled && this._shouldEnable()) {
this.setState({enabled: true});
}
}
};
onRoom = (room) => {
// Always check for membership changes when we see new rooms
this.onRoomMembershipChanged();
const waitingRoom = this._waitingRoomQueue.find(r => r.roomId === room.roomId);
if (!waitingRoom) return;
this._waitingRoomQueue.splice(this._waitingRoomQueue.indexOf(waitingRoom), 1);
const now = (new Date()).getTime();
if ((now - waitingRoom.addedTs) > AUTOJOIN_WAIT_THRESHOLD_MS) return; // Too long ago.
this._appendRoomId(room.roomId); // add the room we've been waiting for
};
_shouldEnable() {
const client = MatrixClientPeg.get();

View file

@ -460,13 +460,9 @@ export default class SlateMessageComposer extends React.Component {
const showFormatBar = this.state.showFormatting && this.state.inputState.isRichTextEnabled;
const wrapperClasses = classNames({
mx_MessageComposer_wrapper: true,
mx_MessageComposer_hasE2EIcon: !!this.props.e2eStatus,
});
return (
<div className="mx_MessageComposer">
<div className={wrapperClasses}>
<div className="mx_MessageComposer_wrapper">
<div className="mx_MessageComposer_row">
{ controls }
</div>

View file

@ -74,10 +74,10 @@ export default class Stickerpicker extends React.Component {
this.forceUpdate();
return this.scalarClient;
}).catch((e) => {
this._imError(_td("Failed to connect to integrations server"), e);
this._imError(_td("Failed to connect to integration manager"), e);
});
} else {
this._imError(_td("No integrations server is configured to manage stickers with"));
IntegrationManagers.sharedInstance().openNoManagerDialog();
}
}
@ -287,12 +287,17 @@ export default class Stickerpicker extends React.Component {
return stickersContent;
}
/**
// Dev note: this isn't jsdoc because it's angry.
/*
* Show the sticker picker overlay
* If no stickerpacks have been added, show a link to the integration manager add sticker packs page.
* @param {Event} e Event that triggered the function
*/
_onShowStickersClick(e) {
if (!SettingsStore.getValue("integrationProvisioning")) {
// Intercept this case and spawn a warning.
return IntegrationManagers.sharedInstance().showDisabledDialog();
}
// XXX: Simplify by using a context menu that is positioned relative to the sticker picker button
const buttonRect = e.target.getBoundingClientRect();
@ -346,7 +351,7 @@ export default class Stickerpicker extends React.Component {
}
/**
* Launch the integrations manager on the stickers integration page
* Launch the integration manager on the stickers integration page
*/
_launchManageIntegrations() {
// TODO: Open the right integration manager for the widget

View file

@ -112,7 +112,7 @@ module.exports = createReactClass({
}
});
httpPromise.done(function() {
httpPromise.then(function() {
self.setState({
phase: self.Phases.Display,
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl),

View file

@ -25,7 +25,6 @@ const Modal = require("../../../Modal");
const sdk = require("../../../index");
import dis from "../../../dispatcher";
import Promise from 'bluebird';
import AccessibleButton from '../elements/AccessibleButton';
import { _t } from '../../../languageHandler';
@ -174,21 +173,16 @@ module.exports = createReactClass({
newPassword: "",
newPasswordConfirm: "",
});
}).done();
});
},
_optionallySetEmail: function() {
const deferred = Promise.defer();
// Ask for an email otherwise the user has no way to reset their password
const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog");
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?'),
onFinished: (confirmed) => {
// ignore confirmed, setting an email is optional
deferred.resolve(confirmed);
},
});
return deferred.promise;
return modal.finished.then(([confirmed]) => confirmed);
},
_onExportE2eKeysClicked: function() {

View file

@ -52,7 +52,7 @@ export default class DevicesPanel extends React.Component {
}
_loadDevices() {
MatrixClientPeg.get().getDevices().done(
MatrixClientPeg.get().getDevices().then(
(resp) => {
if (this._unmounted) { return; }
this.setState({devices: resp.devices || []});

View file

@ -21,12 +21,9 @@ import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher';
export default class IntegrationsManager extends React.Component {
export default class IntegrationManager extends React.Component {
static propTypes = {
// false to display an error saying that there is no integrations manager configured
configured: PropTypes.bool.isRequired,
// false to display an error saying that we couldn't connect to the integrations manager
// false to display an error saying that we couldn't connect to the integration manager
connected: PropTypes.bool.isRequired,
// true to display a loading spinner
@ -40,7 +37,6 @@ export default class IntegrationsManager extends React.Component {
};
static defaultProps = {
configured: true,
connected: true,
loading: false,
};
@ -70,20 +66,11 @@ export default class IntegrationsManager extends React.Component {
};
render() {
if (!this.props.configured) {
return (
<div className='mx_IntegrationsManager_error'>
<h3>{_t("No integrations server configured")}</h3>
<p>{_t("This Riot instance does not have an integrations server configured.")}</p>
</div>
);
}
if (this.props.loading) {
const Spinner = sdk.getComponent("elements.Spinner");
return (
<div className='mx_IntegrationsManager_loading'>
<h3>{_t("Connecting to integrations server...")}</h3>
<div className='mx_IntegrationManager_loading'>
<h3>{_t("Connecting to integration manager...")}</h3>
<Spinner />
</div>
);
@ -91,9 +78,9 @@ export default class IntegrationsManager extends React.Component {
if (!this.props.connected) {
return (
<div className='mx_IntegrationsManager_error'>
<h3>{_t("Cannot connect to integrations server")}</h3>
<p>{_t("The integrations server is offline or it cannot reach your homeserver.")}</p>
<div className='mx_IntegrationManager_error'>
<h3>{_t("Cannot connect to integration manager")}</h3>
<p>{_t("The integration manager is offline or it cannot reach your homeserver.")}</p>
</div>
);
}

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from 'react';
import createReactClass from 'create-react-class';
import Promise from 'bluebird';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
@ -30,6 +29,7 @@ import {
} from '../../../notifications';
import SdkConfig from "../../../SdkConfig";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import AccessibleButton from "../elements/AccessibleButton";
// TODO: this "view" component still has far too much application logic in it,
// which should be factored out to other files.
@ -97,7 +97,7 @@ module.exports = createReactClass({
phase: this.phases.LOADING,
});
MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).done(function() {
MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).then(function() {
self._refreshFromServer();
});
},
@ -170,7 +170,7 @@ module.exports = createReactClass({
emailPusher.kind = null;
emailPusherPromise = MatrixClientPeg.get().setPusher(emailPusher);
}
emailPusherPromise.done(() => {
emailPusherPromise.then(() => {
this._refreshFromServer();
}, (error) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -274,7 +274,7 @@ module.exports = createReactClass({
}
}
Promise.all(deferreds).done(function() {
Promise.all(deferreds).then(function() {
self._refreshFromServer();
}, function(error) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -343,7 +343,7 @@ module.exports = createReactClass({
}
}
Promise.all(deferreds).done(function(resps) {
Promise.all(deferreds).then(function(resps) {
self._refreshFromServer();
}, function(error) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -398,7 +398,7 @@ module.exports = createReactClass({
};
// Then, add the new ones
Promise.all(removeDeferreds).done(function(resps) {
Promise.all(removeDeferreds).then(function(resps) {
const deferreds = [];
let pushRuleVectorStateKind = self.state.vectorContentRules.vectorState;
@ -434,7 +434,7 @@ module.exports = createReactClass({
}
}
Promise.all(deferreds).done(function(resps) {
Promise.all(deferreds).then(function(resps) {
self._refreshFromServer();
}, onError);
}, onError);
@ -650,11 +650,22 @@ module.exports = createReactClass({
externalContentRules: self.state.externalContentRules,
externalPushRules: self.state.externalPushRules,
});
}).done();
});
MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids}));
},
_onClearNotifications: function() {
const cli = MatrixClientPeg.get();
cli.getRooms().forEach(r => {
if (r.getUnreadNotificationCount() > 0) {
const events = r.getLiveTimeline().getEvents();
if (events.length) cli.sendReadReceipt(events.pop());
}
});
},
_updatePushRuleActions: function(rule, actions, enabled) {
const cli = MatrixClientPeg.get();
@ -747,6 +758,13 @@ module.exports = createReactClass({
label={_t('Enable notifications for this account')}/>;
}
let clearNotificationsButton;
if (MatrixClientPeg.get().getRooms().some(r => r.getUnreadNotificationCount() > 0)) {
clearNotificationsButton = <AccessibleButton onClick={this._onClearNotifications} kind='danger'>
{_t("Clear notifications")}
</AccessibleButton>;
}
// When enabled, the master rule inhibits all existing rules
// So do not show all notification settings
if (this.state.masterPushRule && this.state.masterPushRule.enabled) {
@ -757,6 +775,8 @@ module.exports = createReactClass({
<div className="mx_UserNotifSettings_notifTable">
{ _t('All notifications are currently disabled for all targets.') }
</div>
{clearNotificationsButton}
</div>
);
}
@ -878,6 +898,7 @@ module.exports = createReactClass({
{ devicesSection }
{ clearNotificationsButton }
</div>
</div>

View file

@ -26,6 +26,7 @@ import { getThreepidsWithBindStatus } from '../../../boundThreepids';
import IdentityAuthClient from "../../../IdentityAuthClient";
import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils";
import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../../utils/IdentityServerUtils';
import {timeout} from "../../../utils/promise";
// We'll wait up to this long when checking for 3PID bindings on the IS.
const REACHABILITY_TIMEOUT = 10000; // ms
@ -245,14 +246,11 @@ export default class SetIdServer extends React.Component {
let threepids = [];
let currentServerReachable = true;
try {
threepids = await Promise.race([
threepids = await timeout(
getThreepidsWithBindStatus(MatrixClientPeg.get()),
new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("Timeout attempting to reach identity server"));
}, REACHABILITY_TIMEOUT);
}),
]);
Promise.reject(new Error("Timeout attempting to reach identity server")),
REACHABILITY_TIMEOUT,
);
} catch (e) {
currentServerReachable = false;
console.warn(

View file

@ -16,13 +16,9 @@ limitations under the License.
import React from 'react';
import {_t} from "../../../languageHandler";
import sdk from '../../../index';
import Field from "../elements/Field";
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import MatrixClientPeg from "../../../MatrixClientPeg";
import {SERVICE_TYPES} from "matrix-js-sdk";
import {IntegrationManagerInstance} from "../../../integrations/IntegrationManagerInstance";
import Modal from "../../../Modal";
import sdk from '../../../index';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
export default class SetIntegrationManager extends React.Component {
constructor() {
@ -32,135 +28,23 @@ export default class SetIntegrationManager extends React.Component {
this.state = {
currentManager,
url: "", // user-entered text
error: null,
busy: false,
checking: false,
provisioningEnabled: SettingsStore.getValue("integrationProvisioning"),
};
}
_onUrlChanged = (ev) => {
const u = ev.target.value;
this.setState({url: u});
};
onProvisioningToggled = () => {
const current = this.state.provisioningEnabled;
SettingsStore.setValue("integrationProvisioning", null, SettingLevel.ACCOUNT, !current).catch(err => {
console.error("Error changing integration manager provisioning");
console.error(err);
_getTooltip = () => {
if (this.state.checking) {
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
return <div>
<InlineSpinner />
{ _t("Checking server") }
</div>;
} else if (this.state.error) {
return <span className="warning">{this.state.error}</span>;
} else {
return null;
}
};
_canChange = () => {
return !!this.state.url && !this.state.busy;
};
_continueTerms = async (manager) => {
try {
await IntegrationManagers.sharedInstance().overwriteManagerOnAccount(manager);
this.setState({
busy: false,
error: null,
currentManager: IntegrationManagers.sharedInstance().getPrimaryManager(),
url: "", // clear input
});
} catch (e) {
console.error(e);
this.setState({
busy: false,
error: _t("Failed to update integration manager"),
});
}
};
_setManager = async (ev) => {
// Don't reload the page when the user hits enter in the form.
ev.preventDefault();
ev.stopPropagation();
this.setState({busy: true, checking: true, error: null});
let offline = false;
let manager: IntegrationManagerInstance;
try {
manager = await IntegrationManagers.sharedInstance().tryDiscoverManager(this.state.url);
offline = !manager; // no manager implies offline
} catch (e) {
console.error(e);
offline = true; // probably a connection error
}
if (offline) {
this.setState({
busy: false,
checking: false,
error: _t("Integration manager offline or not accessible."),
});
return;
}
// Test the manager (causes terms of service prompt if agreement is needed)
// We also cancel the tooltip at this point so it doesn't collide with the dialog.
this.setState({checking: false});
try {
const client = manager.getScalarClient();
await client.connect();
} catch (e) {
console.error(e);
this.setState({
busy: false,
error: _t("Terms of service not accepted or the integration manager is invalid."),
});
return;
}
// Specifically request the terms of service to see if there are any.
// The above won't trigger a terms of service check if there are no terms to
// sign, so when there's no terms at all we need to ensure we tell the user.
let hasTerms = true;
try {
const terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IM, manager.trimmedApiUrl);
hasTerms = terms && terms['policies'] && Object.keys(terms['policies']).length > 0;
} catch (e) {
// Assume errors mean there are no terms. This could be a 404, 500, etc
console.error(e);
hasTerms = false;
}
if (!hasTerms) {
this.setState({busy: false});
const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog");
Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, {
title: _t("Integration manager has no terms of service"),
description: (
<div>
<span className="warning">
{_t("The integration manager you have chosen does not have any terms of service.")}
</span>
<span>
&nbsp;{_t("Only continue if you trust the owner of the server.")}
</span>
</div>
),
button: _t("Continue"),
onFinished: async (confirmed) => {
if (!confirmed) return;
this._continueTerms(manager);
},
});
return;
}
this._continueTerms(manager);
this.setState({provisioningEnabled: current});
});
this.setState({provisioningEnabled: !current});
};
render() {
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
const ToggleSwitch = sdk.getComponent("views.elements.ToggleSwitch");
const currentManager = this.state.currentManager;
let managerName;
@ -168,45 +52,32 @@ export default class SetIntegrationManager extends React.Component {
if (currentManager) {
managerName = `(${currentManager.name})`;
bodyText = _t(
"You are currently using <b>%(serverName)s</b> to manage your bots, widgets, " +
"Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, " +
"and sticker packs.",
{serverName: currentManager.name},
{ b: sub => <b>{sub}</b> },
);
} else {
bodyText = _t(
"Add which integration manager you want to manage your bots, widgets, " +
"and sticker packs.",
);
bodyText = _t("Use an Integration Manager to manage bots, widgets, and sticker packs.");
}
return (
<form className="mx_SettingsTab_section mx_SetIntegrationManager" onSubmit={this._setManager}>
<div className='mx_SetIntegrationManager'>
<div className="mx_SettingsTab_heading">
<span>{_t("Integration Manager")}</span>
<span>{_t("Manage integrations")}</span>
<span className="mx_SettingsTab_subheading">{managerName}</span>
<ToggleSwitch checked={this.state.provisioningEnabled} onChange={this.onProvisioningToggled} />
</div>
<span className="mx_SettingsTab_subsectionText">
{bodyText}
<br />
<br />
{_t(
"Integration Managers receive configuration data, and can modify widgets, " +
"send room invites, and set power levels on your behalf.",
)}
</span>
<Field
label={_t("Enter a new integration manager")}
id="mx_SetIntegrationManager_newUrl"
type="text" value={this.state.url}
autoComplete="off"
onChange={this._onUrlChanged}
tooltipContent={this._getTooltip()}
tooltipClassName="mx_SetIntegrationManager_tooltip"
disabled={this.state.busy}
flagInvalid={!!this.state.error}
/>
<AccessibleButton
kind="primary_sm"
type="submit"
disabled={!this._canChange()}
onClick={this._setManager}
>{_t("Change")}</AccessibleButton>
</form>
</div>
);
}
}

View file

@ -27,7 +27,7 @@ import LanguageDropdown from "../../../elements/LanguageDropdown";
import AccessibleButton from "../../../elements/AccessibleButton";
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
import PropTypes from "prop-types";
import {enumerateThemes} from "../../../../../theme";
import {enumerateThemes, ThemeWatcher} from "../../../../../theme";
import PlatformPeg from "../../../../../PlatformPeg";
import MatrixClientPeg from "../../../../../MatrixClientPeg";
import sdk from "../../../../..";
@ -50,6 +50,7 @@ export default class GeneralUserSettingsTab extends React.Component {
this.state = {
language: languageHandler.getCurrentLanguage(),
theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"),
useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"),
haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()),
serverSupportsSeparateAddAndBind: null,
idServerHasUnsignedTerms: false,
@ -173,11 +174,29 @@ export default class GeneralUserSettingsTab extends React.Component {
const newTheme = e.target.value;
if (this.state.theme === newTheme) return;
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme);
// doing getValue in the .catch will still return the value we failed to set,
// so remember what the value was before we tried to set it so we can revert
const oldTheme = SettingsStore.getValue('theme');
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => {
dis.dispatch({action: 'recheck_theme'});
this.setState({theme: oldTheme});
});
this.setState({theme: newTheme});
dis.dispatch({action: 'set_theme', value: newTheme});
// The settings watcher doesn't fire until the echo comes back from the
// server, so to make the theme change immediately we need to manually
// do the dispatch now
// XXX: The local echoed value appears to be unreliable, in particular
// when settings custom themes(!) so adding forceTheme to override
// the value from settings.
dis.dispatch({action: 'recheck_theme', forceTheme: newTheme});
};
_onUseSystemThemeChanged = (checked) => {
this.setState({useSystemTheme: checked});
dis.dispatch({action: 'recheck_theme'});
}
_onPasswordChangeError = (err) => {
// TODO: Figure out a design that doesn't involve replacing the current dialog
let errMsg = err.error || "";
@ -288,11 +307,24 @@ export default class GeneralUserSettingsTab extends React.Component {
_renderThemeSection() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
const themeWatcher = new ThemeWatcher();
let systemThemeSection;
if (themeWatcher.isSystemThemeSupported()) {
systemThemeSection = <div>
<SettingsFlag name="use_system_theme" level={SettingLevel.DEVICE}
onChange={this._onUseSystemThemeChanged}
/>
</div>;
}
return (
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_themeSection">
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
{systemThemeSection}
<Field id="theme" label={_t("Theme")} element="select"
value={this.state.theme} onChange={this._onThemeChange}>
value={this.state.theme} onChange={this._onThemeChange}
disabled={this.state.useSystemTheme}
>
{Object.entries(enumerateThemes()).map(([theme, text]) => {
return <option key={theme} value={theme}>{text}</option>;
})}

View file

@ -75,7 +75,7 @@ export default class HelpUserSettingsTab extends React.Component {
// stopping in the middle of the logs.
console.log("Clear cache & reload clicked");
MatrixClientPeg.get().stopClient();
MatrixClientPeg.get().store.deleteAllData().done(() => {
MatrixClientPeg.get().store.deleteAllData().then(() => {
PlatformPeg.get().reload();
});
};

View file

@ -0,0 +1,329 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import {_t} from "../../../../../languageHandler";
import {Mjolnir} from "../../../../../mjolnir/Mjolnir";
import {ListRule} from "../../../../../mjolnir/ListRule";
import {BanList, RULE_SERVER, RULE_USER} from "../../../../../mjolnir/BanList";
import Modal from "../../../../../Modal";
import MatrixClientPeg from "../../../../../MatrixClientPeg";
const sdk = require("../../../../..");
export default class MjolnirUserSettingsTab extends React.Component {
constructor() {
super();
this.state = {
busy: false,
newPersonalRule: "",
newList: "",
};
}
_onPersonalRuleChanged = (e) => {
this.setState({newPersonalRule: e.target.value});
};
_onNewListChanged = (e) => {
this.setState({newList: e.target.value});
};
_onAddPersonalRule = async (e) => {
e.preventDefault();
e.stopPropagation();
let kind = RULE_SERVER;
if (this.state.newPersonalRule.startsWith("@")) {
kind = RULE_USER;
}
this.setState({busy: true});
try {
const list = await Mjolnir.sharedInstance().getOrCreatePersonalList();
await list.banEntity(kind, this.state.newPersonalRule, _t("Ignored/Blocked"));
this.setState({newPersonalRule: ""}); // this will also cause the new rule to be rendered
} catch (e) {
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to add Mjolnir rule', '', ErrorDialog, {
title: _t('Error adding ignored user/server'),
description: _t('Something went wrong. Please try again or view your console for hints.'),
});
} finally {
this.setState({busy: false});
}
};
_onSubscribeList = async (e) => {
e.preventDefault();
e.stopPropagation();
this.setState({busy: true});
try {
const room = await MatrixClientPeg.get().joinRoom(this.state.newList);
await Mjolnir.sharedInstance().subscribeToList(room.roomId);
this.setState({newList: ""}); // this will also cause the new rule to be rendered
} catch (e) {
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to subscribe to Mjolnir list', '', ErrorDialog, {
title: _t('Error subscribing to list'),
description: _t('Please verify the room ID or alias and try again.'),
});
} finally {
this.setState({busy: false});
}
};
async _removePersonalRule(rule: ListRule) {
this.setState({busy: true});
try {
const list = Mjolnir.sharedInstance().getPersonalList();
await list.unbanEntity(rule.kind, rule.entity);
} catch (e) {
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to remove Mjolnir rule', '', ErrorDialog, {
title: _t('Error removing ignored user/server'),
description: _t('Something went wrong. Please try again or view your console for hints.'),
});
} finally {
this.setState({busy: false});
}
}
async _unsubscribeFromList(list: BanList) {
this.setState({busy: true});
try {
await Mjolnir.sharedInstance().unsubscribeFromList(list.roomId);
await MatrixClientPeg.get().leave(list.roomId);
} catch (e) {
console.error(e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to unsubscribe from Mjolnir list', '', ErrorDialog, {
title: _t('Error unsubscribing from list'),
description: _t('Please try again or view your console for hints.'),
});
} finally {
this.setState({busy: false});
}
}
_viewListRules(list: BanList) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const room = MatrixClientPeg.get().getRoom(list.roomId);
const name = room ? room.name : list.roomId;
const renderRules = (rules: ListRule[]) => {
if (rules.length === 0) return <i>{_t("None")}</i>;
const tiles = [];
for (const rule of rules) {
tiles.push(<li key={rule.kind + rule.entity}><code>{rule.entity}</code></li>);
}
return <ul>{tiles}</ul>;
};
Modal.createTrackedDialog('View Mjolnir list rules', '', QuestionDialog, {
title: _t("Ban list rules - %(roomName)s", {roomName: name}),
description: (
<div>
<h3>{_t("Server rules")}</h3>
{renderRules(list.serverRules)}
<h3>{_t("User rules")}</h3>
{renderRules(list.userRules)}
</div>
),
button: _t("Close"),
hasCancelButton: false,
});
}
_renderPersonalBanListRules() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const list = Mjolnir.sharedInstance().getPersonalList();
const rules = list ? [...list.userRules, ...list.serverRules] : [];
if (!list || rules.length <= 0) return <i>{_t("You have not ignored anyone.")}</i>;
const tiles = [];
for (const rule of rules) {
tiles.push(
<li key={rule.entity} className="mx_MjolnirUserSettingsTab_listItem">
<AccessibleButton
kind="danger_sm"
onClick={() => this._removePersonalRule(rule)}
disabled={this.state.busy}
>
{_t("Remove")}
</AccessibleButton>&nbsp;
<code>{rule.entity}</code>
</li>,
);
}
return (
<div>
<p>{_t("You are currently ignoring:")}</p>
<ul>{tiles}</ul>
</div>
);
}
_renderSubscribedBanLists() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const personalList = Mjolnir.sharedInstance().getPersonalList();
const lists = Mjolnir.sharedInstance().lists.filter(b => {
return personalList? personalList.roomId !== b.roomId : true;
});
if (!lists || lists.length <= 0) return <i>{_t("You are not subscribed to any lists")}</i>;
const tiles = [];
for (const list of lists) {
const room = MatrixClientPeg.get().getRoom(list.roomId);
const name = room ? <span>{room.name} (<code>{list.roomId}</code>)</span> : <code>list.roomId</code>;
tiles.push(
<li key={list.roomId} className="mx_MjolnirUserSettingsTab_listItem">
<AccessibleButton
kind="danger_sm"
onClick={() => this._unsubscribeFromList(list)}
disabled={this.state.busy}
>
{_t("Unsubscribe")}
</AccessibleButton>&nbsp;
<AccessibleButton
kind="primary_sm"
onClick={() => this._viewListRules(list)}
disabled={this.state.busy}
>
{_t("View rules")}
</AccessibleButton>&nbsp;
{name}
</li>,
);
}
return (
<div>
<p>{_t("You are currently subscribed to:")}</p>
<ul>{tiles}</ul>
</div>
);
}
render() {
const Field = sdk.getComponent('elements.Field');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<div className="mx_SettingsTab mx_MjolnirUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Ignored users")}</div>
<div className="mx_SettingsTab_section">
<div className='mx_SettingsTab_subsectionText'>
<span className='warning'>{_t("⚠ These settings are meant for advanced users.")}</span><br />
<br />
{_t(
"Add users and servers you want to ignore here. Use asterisks " +
"to have Riot match any characters. For example, <code>@bot:*</code> " +
"would ignore all users that have the name 'bot' on any server.",
{}, {code: (s) => <code>{s}</code>},
)}<br />
<br />
{_t(
"Ignoring people is done through ban lists which contain rules for " +
"who to ban. Subscribing to a ban list means the users/servers blocked by " +
"that list will be hidden from you.",
)}
</div>
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Personal ban list")}</span>
<div className='mx_SettingsTab_subsectionText'>
{_t(
"Your personal ban list holds all the users/servers you personally don't " +
"want to see messages from. After ignoring your first user/server, a new room " +
"will show up in your room list named 'My Ban List' - stay in this room to keep " +
"the ban list in effect.",
)}
</div>
<div>
{this._renderPersonalBanListRules()}
</div>
<div>
<form onSubmit={this._onAddPersonalRule} autoComplete="off">
<Field
id="mx_MjolnirUserSettingsTab_personalAdd"
type="text"
label={_t("Server or user ID to ignore")}
placeholder={_t("eg: @bot:* or example.org")}
value={this.state.newPersonalRule}
onChange={this._onPersonalRuleChanged}
/>
<AccessibleButton
type="submit"
kind="primary"
onClick={this._onAddPersonalRule}
disabled={this.state.busy}
>
{_t("Ignore")}
</AccessibleButton>
</form>
</div>
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Subscribed lists")}</span>
<div className='mx_SettingsTab_subsectionText'>
<span className='warning'>{_t("Subscribing to a ban list will cause you to join it!")}</span>
&nbsp;
<span>{_t(
"If this isn't what you want, please use a different tool to ignore users.",
)}</span>
</div>
<div>
{this._renderSubscribedBanLists()}
</div>
<div>
<form onSubmit={this._onSubscribeList} autoComplete="off">
<Field
id="mx_MjolnirUserSettingsTab_subscriptionAdd"
type="text"
label={_t("Room ID or alias of ban list")}
value={this.state.newList}
onChange={this._onNewListChanged}
/>
<AccessibleButton
type="submit"
kind="primary"
onClick={this._onSubscribeList}
disabled={this.state.busy}
>
{_t("Subscribe")}
</AccessibleButton>
</form>
</div>
</div>
</div>
);
}
}

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