Merge branch 'develop' into jryans/4s-new-key-backup

This commit is contained in:
Travis Ralston 2019-11-21 10:22:13 -07:00
commit 72f0401589
142 changed files with 2121 additions and 807 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

@ -382,7 +382,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 +495,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';

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';
@ -525,7 +525,7 @@ export function logout() {
console.log("Failed to call logout API: token will not be invalidated");
onLoggedOut();
},
).done();
);
}
export function softLogout() {
@ -589,6 +589,7 @@ async function startMatrixClient(startSyncing=true) {
if (startSyncing) {
await MatrixClientPeg.start();
await EventIndexPeg.init();
} else {
console.warn("Caller requested only auxiliary services be started");
await MatrixClientPeg.assign();
@ -607,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) {
@ -632,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();
}
/**
@ -649,6 +652,7 @@ export function stopMatrixClient(unsetClient=true) {
IntegrationManagers.sharedInstance().stopWatching();
Mjolnir.sharedInstance().stop();
if (DMRoomMap.shared()) DMRoomMap.shared().stop();
EventIndexPeg.stop();
const cli = MatrixClientPeg.get();
if (cli) {
cli.stopClient();
@ -656,6 +660,7 @@ export function stopMatrixClient(unsetClient=true) {
if (unsetClient) {
MatrixClientPeg.unset();
EventIndexPeg.unset();
}
}
}

View file

@ -23,7 +23,6 @@ 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";

View file

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

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

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

@ -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,6 @@ 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 = {

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';
@ -637,7 +636,7 @@ export default createReactClass({
title: _t('Error'),
description: _t('Failed to upload image'),
});
}).done();
});
},
_onJoinableChange: function(ev) {
@ -676,7 +675,7 @@ export default createReactClass({
this.setState({
avatarChanged: false,
});
}).done();
});
},
_saveGroup: async function() {

View file

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

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,14 +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";
import { ThemeWatcher } from "../../theme";
import { storeRoomAliasInCache } from '../../RoomAliasCache';
import { defer } from "../../utils/promise";
// Disable warnings for now: we use deprecated bluebird functions
// and need to migrate, but they spam the console with warnings.
Promise.config({warnings: false});
/** constants for MatrixChat.state.view */
const VIEWS = {
// a special initial state which is only used at startup, while we are
@ -274,6 +268,8 @@ export default createReactClass({
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this._themeWatcher = new ThemeWatcher();
this._themeWatcher.start();
this.focusComposer = false;
@ -360,6 +356,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);
@ -542,7 +539,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'});
@ -663,9 +660,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
@ -863,7 +857,7 @@ 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) {
@ -980,7 +974,7 @@ export default createReactClass({
const [shouldCreate, createOpts] = await modal.finished;
if (shouldCreate) {
createRoom({createOpts}).done();
createRoom({createOpts});
}
},
@ -1376,17 +1370,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) => {
@ -1756,7 +1739,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'});

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';
@ -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,

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();
@ -1076,6 +1075,7 @@ const TimelinePanel = createReactClass({
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);

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

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

View file

@ -160,7 +160,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) {
@ -190,7 +190,7 @@ 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 sleep(500).then(() => {

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

@ -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,
});

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

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

@ -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,8 @@ 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";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false;
@ -52,7 +53,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 +70,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 +82,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 +208,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));
@ -269,7 +272,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 +294,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 +361,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.log("Revoke widget permissions - %s", this.props.id);
this._revokeWidgetPermission();
}
/**
@ -446,24 +445,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 +541,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 +603,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 +623,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>
@ -627,13 +683,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 +721,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 +745,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

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

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

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

@ -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";
@ -115,7 +114,7 @@ module.exports = createReactClass({
this.setState({
error: err,
});
}).done();
});
}
},

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

@ -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,7 +21,6 @@ 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";

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;
@ -581,7 +581,7 @@ module.exports = createReactClass({
},
).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
}).done();
});
},
onPowerChange: async function(powerLevel) {
@ -638,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() {

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

@ -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,7 +173,7 @@ module.exports = createReactClass({
newPassword: "",
newPasswordConfirm: "",
});
}).done();
});
},
_optionallySetEmail: 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';
@ -97,7 +96,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 +169,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 +273,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 +342,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 +397,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 +433,7 @@ module.exports = createReactClass({
}
}
Promise.all(deferreds).done(function(resps) {
Promise.all(deferreds).then(function(resps) {
self._refreshFromServer();
}, onError);
}, onError);
@ -650,7 +649,7 @@ module.exports = createReactClass({
externalContentRules: self.state.externalContentRules,
externalPushRules: self.state.externalPushRules,
});
}).done();
});
MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids}));
},

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

@ -21,7 +21,6 @@ import { _t } from './languageHandler';
import dis from "./dispatcher";
import * as Rooms from "./Rooms";
import Promise from 'bluebird';
import {getAddressType} from "./UserAddress";
/**

View file

@ -340,10 +340,10 @@
"Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)",
"Render simple counters in room header": "Render simple counters in room header",
"Multiple integration managers": "Multiple integration managers",
"Use the new, consistent UserInfo panel for Room Members and Group Members": "Use the new, consistent UserInfo panel for Room Members and Group Members",
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
"Send verification requests in direct message": "Send verification requests in direct message",
"Send verification requests in direct message, including a new verification UX in the member panel.": "Send verification requests in direct message, including a new verification UX in the member panel.",
"Enable cross-signing to verify per-user instead of per-device": "Enable cross-signing to verify per-user instead of per-device",
"Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)",
"Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages",
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
"Use compact timeline layout": "Use compact timeline layout",
@ -364,6 +364,7 @@
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
"Mirror local video feed": "Mirror local video feed",
"Enable Community Filter Panel": "Enable Community Filter Panel",
"Match system dark mode setting": "Match system dark mode setting",
"Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls",
"Send analytics data": "Send analytics data",
"Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device",
@ -512,6 +513,9 @@
"Cannot connect to integrations server": "Cannot connect to integrations server",
"The integrations server is offline or it cannot reach your homeserver.": "The integrations server is offline or it cannot reach your homeserver.",
"Send cross-signing keys to homeserver": "Send cross-signing keys to homeserver",
"Connecting to integration manager...": "Connecting to integration manager...",
"Cannot connect to integration manager": "Cannot connect to integration manager",
"The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",
"Delete Backup": "Delete Backup",
"Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.",
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.",
@ -599,15 +603,10 @@
"Do not use an identity server": "Do not use an identity server",
"Enter a new identity server": "Enter a new identity server",
"Change": "Change",
"Failed to update integration manager": "Failed to update integration manager",
"Integration manager offline or not accessible.": "Integration manager offline or not accessible.",
"Terms of service not accepted or the integration manager is invalid.": "Terms of service not accepted or the integration manager is invalid.",
"Integration manager has no terms of service": "Integration manager has no terms of service",
"The integration manager you have chosen does not have any terms of service.": "The integration manager you have chosen does not have any terms of service.",
"You are currently using <b>%(serverName)s</b> to manage your bots, widgets, and sticker packs.": "You are currently using <b>%(serverName)s</b> to manage your bots, widgets, and sticker packs.",
"Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Add which integration manager you want to manage your bots, widgets, and sticker packs.",
"Integration Manager": "Integration Manager",
"Enter a new integration manager": "Enter a new integration manager",
"Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.",
"Use an Integration Manager to manage bots, widgets, and sticker packs.": "Use an Integration Manager to manage bots, widgets, and sticker packs.",
"Manage integrations": "Manage integrations",
"Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.",
"Flair": "Flair",
"Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
"Success": "Success",
@ -1025,8 +1024,7 @@
"numbered-list": "numbered-list",
"Show Text Formatting Toolbar": "Show Text Formatting Toolbar",
"Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar",
"Failed to connect to integrations server": "Failed to connect to integrations server",
"No integrations server is configured to manage stickers with": "No integrations server is configured to manage stickers with",
"Failed to connect to integration manager": "Failed to connect to integration manager",
"You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled",
"Add some now": "Add some now",
"Stickerpack": "Stickerpack",
@ -1186,10 +1184,19 @@
"Quick Reactions": "Quick Reactions",
"Cancel search": "Cancel search",
"Unknown Address": "Unknown Address",
"NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted",
"Warning: This widget might use cookies.": "Warning: This widget might use cookies.",
"Do you want to load widget from URL:": "Do you want to load widget from URL:",
"Allow": "Allow",
"Any of the following data may be shared:": "Any of the following data may be shared:",
"Your display name": "Your display name",
"Your avatar URL": "Your avatar URL",
"Your user ID": "Your user ID",
"Your theme": "Your theme",
"Riot URL": "Riot URL",
"Room ID": "Room ID",
"Widget ID": "Widget ID",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.": "Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s.": "Using this widget may share data <helpIcon /> with %(widgetDomain)s.",
"Widgets do not use message encryption.": "Widgets do not use message encryption.",
"Widget added by": "Widget added by",
"This widget may use cookies.": "This widget may use cookies.",
"Delete Widget": "Delete Widget",
"Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?",
"Delete widget": "Delete widget",
@ -1197,10 +1204,8 @@
"An error ocurred whilst trying to remove the widget from the room": "An error ocurred whilst trying to remove the widget from the room",
"Minimize apps": "Minimize apps",
"Maximize apps": "Maximize apps",
"Reload widget": "Reload widget",
"Popout widget": "Popout widget",
"Picture": "Picture",
"Revoke widget access": "Revoke widget access",
"More options": "More options",
"Create new room": "Create new room",
"Unblacklist": "Unblacklist",
"Blacklist": "Blacklist",
@ -1391,6 +1396,10 @@
"Verifying this user will mark their device as trusted, and also mark your device as trusted to them.": "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.",
"Waiting for partner to confirm...": "Waiting for partner to confirm...",
"Incoming Verification Request": "Incoming Verification Request",
"Integrations are disabled": "Integrations are disabled",
"Enable 'Manage Integrations' in Settings to do this.": "Enable 'Manage Integrations' in Settings to do this.",
"Integrations not allowed": "Integrations not allowed",
"Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Your Riot doesn't allow you to use an Integration Manager to do this. Please contact an admin.",
"You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.",
"Your unverified device '%(displayName)s' is requesting encryption keys.": "Your unverified device '%(displayName)s' is requesting encryption keys.",
"Start verification": "Start verification",
@ -1468,7 +1477,7 @@
"Missing session data": "Missing session data",
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.",
"Your browser likely removed this data when running low on disk space.": "Your browser likely removed this data when running low on disk space.",
"Integrations Manager": "Integrations Manager",
"Integration Manager": "Integration Manager",
"Find others by phone or email": "Find others by phone or email",
"Be found by phone or email": "Be found by phone or email",
"Use bots, bridges, widgets and sticker packs": "Use bots, bridges, widgets and sticker packs",
@ -1497,6 +1506,7 @@
"A widget would like to verify your identity": "A widget would like to verify your identity",
"A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.",
"Remember my selection for this widget": "Remember my selection for this widget",
"Allow": "Allow",
"Deny": "Deny",
"Unable to load backup status": "Unable to load backup status",
"Recovery Key Mismatch": "Recovery Key Mismatch",
@ -1556,6 +1566,10 @@
"Hide": "Hide",
"Home": "Home",
"Sign in": "Sign in",
"Reload": "Reload",
"Take picture": "Take picture",
"Remove for everyone": "Remove for everyone",
"Remove for me": "Remove for me",
"powered by Matrix": "powered by Matrix",
"This homeserver would like to make sure you are not a robot.": "This homeserver would like to make sure you are not a robot.",
"Custom Server Options": "Custom Server Options",

View file

@ -2337,5 +2337,19 @@
"<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> dans %(roomName)s",
"Messages in this room are end-to-end encrypted.": "Les messages dans ce salon sont chiffrés de bout en bout.",
"Security": "Sécurité",
"Verify": "Vérifier"
"Verify": "Vérifier",
"Enable cross-signing to verify per-user instead of per-device": "Activer la signature croisée pour vérifier par utilisateur et non par appareil",
"Any of the following data may be shared:": "Les données suivants peuvent être partagées :",
"Your display name": "Votre nom daffichage",
"Your avatar URL": "LURL de votre avatar",
"Your user ID": "Votre identifiant utilisateur",
"Your theme": "Votre thème",
"Riot URL": "URL de Riot",
"Room ID": "Identifiant du salon",
"Widget ID": "Identifiant du widget",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.": "Lutilisation de ce widget pourrait partager des données <helpIcon /> avec %(widgetDomain)s et votre gestionnaire dintégrations.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s.": "Lutilisation de ce widget pourrait partager des données <helpIcon /> avec %(widgetDomain)s.",
"Widget added by": "Widget ajouté par",
"This widget may use cookies.": "Ce widget pourrait utiliser des cookies.",
"Send verification requests in direct message, including a new verification UX in the member panel.": "Envoyer les demandes de vérification en message direct, en incluant une nouvelle expérience de vérification dans le tableau des membres."
}

View file

@ -2324,5 +2324,19 @@
"<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> a szobában: %(roomName)s",
"Messages in this room are end-to-end encrypted.": "Az üzenetek a szobában végponttól végpontig titkosítottak.",
"Security": "Biztonság",
"Verify": "Ellenőriz"
"Verify": "Ellenőriz",
"Enable cross-signing to verify per-user instead of per-device": "Kereszt-aláírás engedélyezése eszköz alapú ellenőrzés helyett felhasználó alapú ellenőrzéshez",
"Any of the following data may be shared:": "Az alábbi adatok közül bármelyik megosztásra kerülhet:",
"Your display name": "Megjelenítési neved",
"Your avatar URL": "Profilképed URL-je",
"Your user ID": "Felhasználói azonosítód",
"Your theme": "Témád",
"Riot URL": "Riot URL",
"Room ID": "Szoba azonosító",
"Widget ID": "Kisalkalmazás azon.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg <helpIcon /> %(widgetDomain)s domain-nel és az Integrációs Menedzserrel.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg <helpIcon /> %(widgetDomain)s domain-nel.",
"Widget added by": "A kisalkalmazást hozzáadta",
"This widget may use cookies.": "Ez a kisalkalmazás sütiket használhat.",
"Send verification requests in direct message, including a new verification UX in the member panel.": "Ellenőrzés küldése közvetlen üzenetben, beleértve az új ellenőrzési felhasználói élményt a résztvevői panelen."
}

View file

@ -2270,5 +2270,30 @@
"You cancelled": "Hai annullato",
"%(name)s cancelled": "%(name)s ha annullato",
"%(name)s wants to verify": "%(name)s vuole verificare",
"You sent a verification request": "Hai inviato una richiesta di verifica"
"You sent a verification request": "Hai inviato una richiesta di verifica",
"Custom (%(level)s)": "Personalizzato (%(level)s)",
"Trusted": "Fidato",
"Not trusted": "Non fidato",
"Hide verified Sign-In's": "Nascondi accessi verificati",
"%(count)s verified Sign-In's|other": "%(count)s accessi verificati",
"%(count)s verified Sign-In's|one": "1 accesso verificato",
"Direct message": "Messaggio diretto",
"Unverify user": "Revoca verifica utente",
"<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> in %(roomName)s",
"Messages in this room are end-to-end encrypted.": "I messaggi in questa stanza sono cifrati end-to-end.",
"Security": "Sicurezza",
"Verify": "Verifica",
"Enable cross-signing to verify per-user instead of per-device": "Attiva la firma incrociata per verificare per-utente invece che per-dispositivo",
"Any of the following data may be shared:": "Possono essere condivisi tutti i seguenti dati:",
"Your display name": "Il tuo nome visualizzato",
"Your avatar URL": "L'URL del tuo avatar",
"Your user ID": "Il tuo ID utente",
"Your theme": "Il tuo tema",
"Riot URL": "URL di Riot",
"Room ID": "ID stanza",
"Widget ID": "ID widget",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.": "Usando questo widget i dati possono essere condivisi <helpIcon /> con %(widgetDomain)s e il tuo Gestore di Integrazione.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s.": "Usando questo widget i dati possono essere condivisi <helpIcon /> con %(widgetDomain)s.",
"Widget added by": "Widget aggiunto da",
"This widget may use cookies.": "Questo widget può usare cookie."
}

View file

@ -2181,5 +2181,19 @@
"Messages in this room are end-to-end encrypted.": "이 방의 메시지는 종단간 암호화되었습니다.",
"Security": "보안",
"Verify": "확인",
"You have ignored this user, so their message is hidden. <a>Show anyways.</a>": "이 사용자를 무시했습니다. 사용자의 메시지는 숨겨집니다. <a>무시하고 보이기.</a>"
"You have ignored this user, so their message is hidden. <a>Show anyways.</a>": "이 사용자를 무시했습니다. 사용자의 메시지는 숨겨집니다. <a>무시하고 보이기.</a>",
"Send verification requests in direct message, including a new verification UX in the member panel.": "다이렉트 메시지에서 구성원 패널에 새 확인 UX가 적용된 확인 요청을 보냅니다.",
"Enable cross-signing to verify per-user instead of per-device": "기기 당 확인이 아닌 사람 당 확인을 위한 교차 서명 켜기",
"Any of the following data may be shared:": "다음 데이터가 공유됩니다:",
"Your display name": "당신의 표시 이름",
"Your avatar URL": "당신의 아바타 URL",
"Your user ID": "당신의 사용자 ID",
"Your theme": "당신의 테마",
"Riot URL": "Riot URL",
"Room ID": "방 ID",
"Widget ID": "위젯 ID",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.": "이 위젯을 사용하면 <helpcon /> %(widgetDomain)s & 통합 관리자와 데이터를 공유합니다.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s.": "이 위젯을 사용하면 <helpIcon /> %(widgetDomain)s와(과) 데이터를 공유합니다.",
"Widget added by": "위젯을 추가했습니다",
"This widget may use cookies.": "이 위젯은 쿠키를 사용합니다."
}

View file

@ -320,7 +320,7 @@
"Mobile phone number (optional)": "Numer telefonu komórkowego (opcjonalne)",
"Moderator": "Moderator",
"%(serverName)s Matrix ID": "%(serverName)s Matrix ID",
"Name": "Imię",
"Name": "Nazwa",
"Never send encrypted messages to unverified devices from this device": "Nigdy nie wysyłaj zaszyfrowanych wiadomości do niezweryfikowanych urządzeń z tego urządzenia",
"Never send encrypted messages to unverified devices in this room from this device": "Nigdy nie wysyłaj niezaszyfrowanych wiadomości do niezweryfikowanych urządzeń z tego urządzenia",
"New address (e.g. #foo:%(localDomain)s)": "Nowy adres (np. #foo:%(localDomain)s)",
@ -972,7 +972,7 @@
"Disinvite this user?": "Anulować zaproszenie tego użytkownika?",
"Unignore": "Przestań ignorować",
"Jump to read receipt": "Przeskocz do potwierdzenia odczytu",
"Share Link to User": "Udostępnij link do użytkownika",
"Share Link to User": "Udostępnij odnośnik do użytkownika",
"At this time it is not possible to reply with a file so this will be sent without being a reply.": "W tej chwili nie można odpowiedzieć plikiem, więc zostanie wysłany nie będąc odpowiedzią.",
"Unable to reply": "Nie udało się odpowiedzieć",
"At this time it is not possible to reply with an emote.": "W tej chwili nie można odpowiedzieć emotikoną.",
@ -1492,7 +1492,7 @@
"You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Możesz się zarejestrować, lecz niektóre funkcje nie będą dostępne dopóki Serwer Tożsamości nie będzie znów online. Jeśli ciągle widzisz to ostrzeżenie, sprawdź swoją konfigurację lub skontaktuj się z administratorem serwera.",
"You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Możesz zresetować hasło, lecz niektóre funkcje nie będą dostępne dopóki Serwer Tożsamości nie będzie znów online. Jeśli ciągle widzisz to ostrzeżenie, sprawdź swoją konfigurację lub skontaktuj się z administratorem serwera.",
"You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Możesz się zalogować, lecz niektóre funkcje nie będą dostępne dopóki Serwer Tożsamości nie będzie znów online. Jeśli ciągle widzisz to ostrzeżenie, sprawdź swoją konfigurację lub skontaktuj się z administratorem serwera.",
"No homeserver URL provided": "Nie podano URL serwera głównego.",
"No homeserver URL provided": "Nie podano URL serwera głównego",
"The server does not support the room version specified.": "Serwer nie wspiera tej wersji pokoju.",
"Name or Matrix ID": "Imię lub identyfikator Matrix",
"Email, name or Matrix ID": "E-mail, imię lub Matrix ID",
@ -1528,7 +1528,7 @@
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s dezaktywował Flair dla %(groups)s w tym pokoju.",
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s aktywował Flair dla %(newGroups)s i dezaktywował Flair dla %(oldGroups)s w tym pokoju.",
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s odwołał zaproszednie dla %(targetDisplayName)s aby dołączył do pokoju.",
"%(names)s and %(count)s others are typing …|one": "%(names)s i jedna osoba pisze.",
"%(names)s and %(count)s others are typing …|one": "%(names)s i jedna osoba pisze",
"Cannot reach homeserver": "Błąd połączenia z serwerem domowym",
"Ensure you have a stable internet connection, or get in touch with the server admin": "Upewnij się, że posiadasz stabilne połączenie internetowe lub skontaktuj się z administratorem serwera",
"Your Riot is misconfigured": "Twój Riot jest źle skonfigurowany",
@ -1556,7 +1556,7 @@
"Order rooms in the room list by most important first instead of most recent": "Kolejkuj pokoje na liście pokojów od najważniejszych niż od najnowszych",
"Show hidden events in timeline": "Pokaż ukryte wydarzenia na linii czasowej",
"Low bandwidth mode": "Tryb wolnej przepustowości",
"Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Powzól na awaryjny serwer wspomagania połączeń turn.matrix.org, gdy Twój serwer domowy takiego nie oferuje (Twój adres IP będzie udostępniony podczas połączenia)",
"Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Pozwól na awaryjny serwer wspomagania połączeń turn.matrix.org, gdy Twój serwer domowy takiego nie oferuje (Twój adres IP będzie udostępniony podczas połączenia)",
"Messages containing my username": "Wiadomości zawierające moją nazwę użytkownika",
"Encrypted messages in one-to-one chats": "Zaszyforwane wiadomości w rozmowach jeden-do-jednego",
"Encrypted messages in group chats": "Zaszyfrowane wiadomości w rozmowach grupowych",
@ -1619,7 +1619,7 @@
"Disconnect Identity Server": "Odłącz Serwer Tożsamości",
"Disconnect": "Odłącz",
"Identity Server (%(server)s)": "Serwer tożsamości (%(server)s)",
"You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Używasz <server></server> aby odkrywać i być odkrywanym przez isteniejące kontakty, które znasz. Możesz zmienić serwer tożsamości poniżej.",
"You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Używasz <server></server>, aby odnajdywać i móc być odnajdywanym przez istniejące kontakty, które znasz. Możesz zmienić serwer tożsamości poniżej.",
"Identity Server": "Serwer Tożsamości",
"You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Nie używasz serwera tożsamości. Aby odkrywać i być odkrywanym przez istniejące kontakty które znasz, dodaj jeden poniżej.",
"Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Odłączenie się od serwera tożsamości oznacza, że nie będzie możliwości wykrycia przez innych użytkowników oraz nie będzie możliwości zaproszenia innych e-mailem lub za pomocą telefonu.",
@ -1653,5 +1653,84 @@
"You do not have the required permissions to use this command.": "Nie posiadasz wymaganych uprawnień do użycia tego polecenia.",
"Changes the avatar of the current room": "Zmienia awatar dla obecnego pokoju",
"Use an identity server": "Użyj serwera tożsamości",
"Show previews/thumbnails for images": "Pokaż podgląd/miniatury obrazów"
"Show previews/thumbnails for images": "Pokaż podgląd/miniatury obrazów",
"Trust": "Zaufaj",
"Custom (%(level)s)": "Własny (%(level)s)",
"Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Użyj serwera tożsamości, by zaprosić z użyciem adresu e-mail. Kliknij dalej, żeby użyć domyślnego serwera tożsamości (%(defaultIdentityServerName)s), lub zmień w Ustawieniach.",
"Use an identity server to invite by email. Manage in Settings.": "Użyj serwera tożsamości, by zaprosić za pomocą adresu e-mail. Zarządzaj w ustawieniach.",
"%(name)s (%(userId)s)": "%(name)s (%(userId)s)",
"Use the new, consistent UserInfo panel for Room Members and Group Members": "Użyj nowego, spójnego panelu informacji o użytkowniku dla członków pokoju i grup",
"Try out new ways to ignore people (experimental)": "Wypróbuj nowe sposoby na ignorowanie ludzi (eksperymentalne)",
"Send verification requests in direct message": "Wysyłaj prośby o weryfikację w bezpośredniej wiadomości",
"Use the new, faster, composer for writing messages": "Używaj nowego, szybszego kompozytora do pisania wiadomości",
"My Ban List": "Moja lista zablokowanych",
"This is your list of users/servers you have blocked - don't leave the room!": "To jest Twoja lista zablokowanych użytkowników/serwerów nie opuszczaj tego pokoju!",
"Change identity server": "Zmień serwer tożsamości",
"Disconnect from the identity server <current /> and connect to <new /> instead?": "Rozłączyć się z serwerem tożsamości <current /> i połączyć się w jego miejsce z <new />?",
"Disconnect identity server": "Odłączanie serwera tożsamości",
"You should:": "Należy:",
"check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "sprawdzić rozszerzenia przeglądarki, które mogą blokować serwer tożsamości (takie jak Privacy Badger)",
"contact the administrators of identity server <idserver />": "skontaktować się z administratorami serwera tożsamości <idserver />",
"wait and try again later": "zaczekaj i spróbuj ponownie później",
"Disconnect anyway": "Odłącz mimo to",
"You are still <b>sharing your personal data</b> on the identity server <idserver />.": "W dalszym ciągu <b>udostępniasz swoje dane osobowe</b> na serwerze tożsamości <idserver />.",
"We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Zalecamy, by usunąć swój adres e-mail i numer telefonu z serwera tożsamości przed odłączeniem.",
"If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "Jeżeli nie chcesz używać <server /> do odnajdywania i bycia odnajdywanym przez osoby, które znasz, wpisz inny serwer tożsamości poniżej.",
"Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Używanie serwera tożsamości jest opcjonalne. Jeżeli postanowisz nie używać serwera tożsamości, pozostali użytkownicy nie będą w stanie Cię odnaleźć ani nie będziesz mógł zaprosić innych po adresie e-mail czy numerze telefonu.",
"Do not use an identity server": "Nie używaj serwera tożsamości",
"Clear cache and reload": "Wyczyść pamięć podręczną i przeładuj",
"Something went wrong. Please try again or view your console for hints.": "Coś poszło nie tak. Spróbuj ponownie lub sprawdź konsolę przeglądarki dla wskazówek.",
"Please verify the room ID or alias and try again.": "Zweryfikuj poprawność ID pokoju lub nazwy zastępczej i spróbuj ponownie.",
"Please try again or view your console for hints.": "Spróbuj ponownie lub sprawdź konsolę przeglądarki dla wskazówek.",
"Personal ban list": "Osobista lista zablokowanych",
"Server or user ID to ignore": "ID serwera lub użytkownika do zignorowania",
"eg: @bot:* or example.org": "np: @bot:* lub przykład.pl",
"Composer": "Kompozytor",
"Autocomplete delay (ms)": "Opóźnienie autouzupełniania (ms)",
"Explore": "Przeglądaj",
"Filter": "Filtruj",
"Add room": "Dodaj pokój",
"A device's public name is visible to people you communicate with": "Publiczna nazwa urządzenia jest widoczna dla ludzi, z którymi się komunikujesz",
"Request media permissions": "Zapytaj o uprawnienia",
"Voice & Video": "Głos & Wideo",
"this room": "ten pokój",
"View older messages in %(roomName)s.": "Wyświetl starsze wiadomości w %(roomName)s.",
"Room information": "Informacje o pokoju",
"Internal room ID:": "Wewnętrzne ID pokoju:",
"Uploaded sound": "Przesłano dźwięk",
"Change history visibility": "Zmień widoczność historii",
"Upgrade the room": "Zaktualizuj pokój",
"Enable room encryption": "Włącz szyfrowanie pokoju",
"Select the roles required to change various parts of the room": "Wybierz role wymagane do zmieniania różnych części pokoju",
"Enable encryption?": "Włączyć szyfrowanie?",
"Your email address hasn't been verified yet": "Twój adres e-mail nie został jeszcze zweryfikowany",
"Verification code": "Kod weryfikacyjny",
"Remove %(email)s?": "Usunąć %(email)s?",
"Remove %(phone)s?": "Usunąć %(phone)s?",
"Some devices in this encrypted room are not trusted": "Niektóre urządzenia w tym zaszyfrowanym pokoju nie są zaufane",
"Loading …": "Ładowanie…",
"Loading room preview": "Wczytywanie podglądu pokoju",
"Try to join anyway": "Spróbuj dołączyć mimo tego",
"You can still join it because this is a public room.": "Możesz mimo to dołączyć, gdyż pokój jest publiczny.",
"This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "To zaproszenie do %(roomName)s zostało wysłane na adres %(email)s, który nie jest przypisany do Twojego konta",
"Link this email with your account in Settings to receive invites directly in Riot.": "Połącz ten adres e-mail z Twoim kontem w Ustawieniach, aby otrzymywać zaproszenia bezpośrednio w Riot.",
"This invite to %(roomName)s was sent to %(email)s": "To zaproszenie do %(roomName)s zostało wysłane do %(email)s",
"Use an identity server in Settings to receive invites directly in Riot.": "Użyj serwera tożsamości w Ustawieniach, aby otrzymywać zaproszenia bezpośrednio w Riot.",
"Do you want to chat with %(user)s?": "Czy chcesz rozmawiać z %(user)s?",
"Do you want to join %(roomName)s?": "Czy chcesz dołączyć do %(roomName)s?",
"<userName/> invited you": "<userName/> zaprosił(a) CIę",
"You're previewing %(roomName)s. Want to join it?": "Przeglądasz %(roomName)s. Czy chcesz dołączyć do pokoju?",
"Not now": "Nie teraz",
"Don't ask me again": "Nie pytaj ponownie",
"%(count)s unread messages including mentions.|other": "%(count)s nieprzeczytanych wiadomości, wliczając wzmianki.",
"%(count)s unread messages including mentions.|one": "1 nieprzeczytana wzmianka.",
"%(count)s unread messages.|other": "%(count)s nieprzeczytanych wiadomości.",
"%(count)s unread messages.|one": "1 nieprzeczytana wiadomość.",
"Unread mentions.": "Nieprzeczytane wzmianki.",
"Unread messages.": "Nieprzeczytane wiadomości.",
"Join": "Dołącz",
"%(creator)s created and configured the room.": "%(creator)s stworzył(a) i skonfigurował(a) pokój.",
"Preview": "Przejrzyj",
"View": "Wyświetl",
"Missing media permissions, click the button below to request.": "Brakuje uprawnień do mediów, kliknij przycisk poniżej, aby o nie zapytać."
}

View file

@ -842,5 +842,11 @@
"Collapse panel": "Colapsar o painel",
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Com o seu navegador atual, a aparência e sensação de uso da aplicação podem estar completamente incorretas, e algumas das funcionalidades poderão não funcionar. Se quiser tentar de qualquer maneira pode continuar, mas está por sua conta com algum problema que possa encontrar!",
"Checking for an update...": "A procurar uma atualização...",
"There are advanced notifications which are not shown here": "Existem notificações avançadas que não são exibidas aqui"
"There are advanced notifications which are not shown here": "Existem notificações avançadas que não são exibidas aqui",
"Add Email Address": "Adicione adresso de e-mail",
"Add Phone Number": "Adicione número de telefone",
"The platform you're on": "A plataforma em que se encontra",
"The version of Riot.im": "A versão do RIOT.im",
"Whether or not you're logged in (we don't record your username)": "Tenha ou não, iniciado sessão (não iremos guardar o seu username)",
"Your language of choice": "O seu idioma de escolha"
}

View file

@ -2018,7 +2018,7 @@
"Create a private room": "Создать приватную комнату",
"Topic (optional)": "Тема (опционально)",
"Make this room public": "Сделать комнату публичной",
"Use the new, faster, composer for writing messages": "Используйте новый, более быстрый, редактор для написания сообщений.",
"Use the new, faster, composer for writing messages": "Используйте новый, более быстрый, редактор для написания сообщений",
"Send read receipts for messages (requires compatible homeserver to disable)": "Отправлять подтверждения о прочтении сообщений (требуется отключение совместимого домашнего сервера)",
"Show previews/thumbnails for images": "Показать превью / миниатюры для изображений",
"Disconnect from the identity server <current /> and connect to <new /> instead?": "Отключиться от сервера идентификации <current /> и вместо этого подключиться к <new />?",
@ -2050,7 +2050,7 @@
"contact the administrators of identity server <idserver />": "связаться с администраторами сервера идентификации <idserver />",
"wait and try again later": "Подождите и повторите попытку позже",
"Error changing power level requirement": "Ошибка изменения требования к уровню прав",
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Произошла ошибка при изменении требований к уровню прав комнаты. Убедитесь, что у вас достаточно прав и попробуйте снова.",
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Произошла ошибка при изменении требований к уровню доступа комнаты. Убедитесь, что у вас достаточно прав и попробуйте снова.",
"Error changing power level": "Ошибка изменения уровня прав",
"An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Произошла ошибка при изменении уровня прав пользователя. Убедитесь, что у вас достаточно прав и попробуйте снова.",
"Unable to revoke sharing for email address": "Не удается отменить общий доступ к адресу электронной почты",
@ -2165,5 +2165,14 @@
"%(count)s unread messages including mentions.|one": "1 непрочитанное упоминание.",
"%(count)s unread messages.|one": "1 непрочитанное сообщение.",
"Unread messages.": "Непрочитанные сообщения.",
"Message Actions": "Сообщение действий"
"Message Actions": "Сообщение действий",
"This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "Это действие требует по умолчанию доступа к серверу идентификации <server/> для подтверждения адреса электронной почты или номера телефона, но у сервера нет никакого пользовательского соглашения.",
"Custom (%(level)s)": "Пользовательский (%(level)s)",
"%(name)s (%(userId)s)": "%(name)s (%(userId)s)",
"Try out new ways to ignore people (experimental)": "Попробуйте новые способы игнорировать людей (экспериментальные)",
"Send verification requests in direct message": "Отправить запросы на подтверждение в прямом сообщении",
"My Ban List": "Мой список запрещенных",
"Ignored/Blocked": "Игнорируемые/Заблокированные",
"Error adding ignored user/server": "Ошибка добавления игнорируемого пользователя/сервера",
"Error subscribing to list": "Ошибка при подписке на список"
}

View file

@ -2292,5 +2292,17 @@
"<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> në %(roomName)s",
"Messages in this room are end-to-end encrypted.": "Mesazhet në këtë dhomë janë të fshehtëzuara skaj-më-skaj.",
"Security": "Siguri",
"Verify": "Verifikoje"
"Verify": "Verifikoje",
"Any of the following data may be shared:": "Mund të ndahen me të tjerët cilado prej të dhënave vijuese:",
"Your display name": "Emri juaj në ekran",
"Your avatar URL": "URL-ja e avatarit tuaj",
"Your user ID": "ID-ja juaj e përdoruesit",
"Your theme": "Tema juaj",
"Riot URL": "URL Riot-i",
"Room ID": "ID dhome",
"Widget ID": "ID widget-i",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.": "Përdorimi i këtij widget-i mund të sjellë ndarje të dhënash <helpIcon /> me %(widgetDomain)s & Përgjegjësin tuaj të Integrimeve.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s.": "Përdorimi i këtij widget-i mund të sjellë ndarje të dhënash <helpIcon /> me %(widgetDomain)s.",
"Widget added by": "Widget i shtuar nga",
"This widget may use cookies.": "Ky <em>widget</em> mund të përdorë <em>cookies</em>."
}

View file

@ -2330,5 +2330,19 @@
"<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> 在 %(roomName)s",
"Messages in this room are end-to-end encrypted.": "在此聊天室中的訊息為端到端加密。",
"Security": "安全",
"Verify": "驗證"
"Verify": "驗證",
"Send verification requests in direct message, including a new verification UX in the member panel.": "在直接訊息中傳送驗證請求,包含成員面板中新的驗證使用者體驗。",
"Enable cross-signing to verify per-user instead of per-device": "啟用交叉簽章以驗證每個使用者而非每個裝置",
"Any of the following data may be shared:": "可能會分享以下資料:",
"Your display name": "您的顯示名稱",
"Your avatar URL": "您的大頭貼 URL",
"Your user ID": "您的使用 ID",
"Your theme": "您的佈景主題",
"Riot URL": "Riot URL",
"Room ID": "聊天室 ID",
"Widget ID": "小工具 ID",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.": "使用這個小工具可能會與 %(widgetDomain)s 以及您的整合管理員分享資料 <helpIcon />。",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s.": "使用這個小工具可能會與 %(widgetDomain)s 分享資料 <helpIcon /> 。",
"Widget added by": "小工具新增由",
"This widget may use cookies.": "這個小工具可能會使用 cookies。"
}

View file

@ -0,0 +1,223 @@
/*
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.
*/
export interface MatrixEvent {
type: string;
sender: string;
content: {};
event_id: string;
origin_server_ts: number;
unsigned: ?{};
room_id: string;
}
export interface MatrixProfile {
avatar_url: string;
displayname: string;
}
export interface CrawlerCheckpoint {
roomId: string;
token: string;
fullCrawl: boolean;
direction: string;
}
export interface ResultContext {
events_before: [MatrixEvent];
events_after: [MatrixEvent];
profile_info: Map<string, MatrixProfile>;
}
export interface ResultsElement {
rank: number;
result: MatrixEvent;
context: ResultContext;
}
export interface SearchResult {
count: number;
results: [ResultsElement];
highlights: [string];
}
export interface SearchArgs {
search_term: string;
before_limit: number;
after_limit: number;
order_by_recency: boolean;
room_id: ?string;
}
export interface HistoricEvent {
event: MatrixEvent;
profile: MatrixProfile;
}
/**
* Base class for classes that provide platform-specific event indexing.
*
* Instances of this class are provided by the application.
*/
export default class BaseEventIndexManager {
/**
* Does our EventIndexManager support event indexing.
*
* If an EventIndexManager implementor has runtime dependencies that
* optionally enable event indexing they may override this method to perform
* the necessary runtime checks here.
*
* @return {Promise} A promise that will resolve to true if event indexing
* is supported, false otherwise.
*/
async supportsEventIndexing(): Promise<boolean> {
return true;
}
/**
* Initialize the event index for the given user.
*
* @return {Promise} A promise that will resolve when the event index is
* initialized.
*/
async initEventIndex(): Promise<> {
throw new Error("Unimplemented");
}
/**
* Queue up an event to be added to the index.
*
* @param {MatrixEvent} ev The event that should be added to the index.
* @param {MatrixProfile} profile The profile of the event sender at the
* time of the event receival.
*
* @return {Promise} A promise that will resolve when the was queued up for
* addition.
*/
async addEventToIndex(ev: MatrixEvent, profile: MatrixProfile): Promise<> {
throw new Error("Unimplemented");
}
/**
* Check if our event index is empty.
*/
indexIsEmpty(): Promise<boolean> {
throw new Error("Unimplemented");
}
/**
* Commit the previously queued up events to the index.
*
* @return {Promise} A promise that will resolve once the queued up events
* were added to the index.
*/
async commitLiveEvents(): Promise<> {
throw new Error("Unimplemented");
}
/**
* Search the event index using the given term for matching events.
*
* @param {SearchArgs} searchArgs The search configuration sets what should
* be searched for and what should be contained in the search result.
*
* @return {Promise<[SearchResult]>} A promise that will resolve to an array
* of search results once the search is done.
*/
async searchEventIndex(searchArgs: SearchArgs): Promise<SearchResult> {
throw new Error("Unimplemented");
}
/**
* Add events from the room history to the event index.
*
* This is used to add a batch of events to the index.
*
* @param {[HistoricEvent]} events The list of events and profiles that
* should be added to the event index.
* @param {[CrawlerCheckpoint]} checkpoint A new crawler checkpoint that
* should be stored in the index which should be used to continue crawling
* the room.
* @param {[CrawlerCheckpoint]} oldCheckpoint The checkpoint that was used
* to fetch the current batch of events. This checkpoint will be removed
* from the index.
*
* @return {Promise} A promise that will resolve to true if all the events
* were already added to the index, false otherwise.
*/
async addHistoricEvents(
events: [HistoricEvent],
checkpoint: CrawlerCheckpoint | null,
oldCheckpoint: CrawlerCheckpoint | null,
): Promise<bool> {
throw new Error("Unimplemented");
}
/**
* Add a new crawler checkpoint to the index.
*
* @param {CrawlerCheckpoint} checkpoint The checkpoint that should be added
* to the index.
*
* @return {Promise} A promise that will resolve once the checkpoint has
* been stored.
*/
async addCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<> {
throw new Error("Unimplemented");
}
/**
* Add a new crawler checkpoint to the index.
*
* @param {CrawlerCheckpoint} checkpoint The checkpoint that should be
* removed from the index.
*
* @return {Promise} A promise that will resolve once the checkpoint has
* been removed.
*/
async removeCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<> {
throw new Error("Unimplemented");
}
/**
* Load the stored checkpoints from the index.
*
* @return {Promise<[CrawlerCheckpoint]>} A promise that will resolve to an
* array of crawler checkpoints once they have been loaded from the index.
*/
async loadCheckpoints(): Promise<[CrawlerCheckpoint]> {
throw new Error("Unimplemented");
}
/**
* close our event index.
*
* @return {Promise} A promise that will resolve once the event index has
* been closed.
*/
async closeEventIndex(): Promise<> {
throw new Error("Unimplemented");
}
/**
* Delete our current event index.
*
* @return {Promise} A promise that will resolve once the event index has
* been deleted.
*/
async deleteEventIndex(): Promise<> {
throw new Error("Unimplemented");
}
}

412
src/indexing/EventIndex.js Normal file
View file

@ -0,0 +1,412 @@
/*
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 PlatformPeg from "../PlatformPeg";
import MatrixClientPeg from "../MatrixClientPeg";
/*
* Event indexing class that wraps the platform specific event indexing.
*/
export default class EventIndex {
constructor() {
this.crawlerCheckpoints = [];
// The time that the crawler will wait between /rooms/{room_id}/messages
// requests
this._crawlerTimeout = 3000;
// The maximum number of events our crawler should fetch in a single
// crawl.
this._eventsPerCrawl = 100;
this._crawler = null;
this.liveEventsForIndex = new Set();
}
async init() {
const indexManager = PlatformPeg.get().getEventIndexingManager();
await indexManager.initEventIndex();
this.registerListeners();
}
registerListeners() {
const client = MatrixClientPeg.get();
client.on('sync', this.onSync);
client.on('Room.timeline', this.onRoomTimeline);
client.on('Event.decrypted', this.onEventDecrypted);
client.on('Room.timelineReset', this.onTimelineReset);
}
removeListeners() {
const client = MatrixClientPeg.get();
if (client === null) return;
client.removeListener('sync', this.onSync);
client.removeListener('Room.timeline', this.onRoomTimeline);
client.removeListener('Event.decrypted', this.onEventDecrypted);
client.removeListener('Room.timelineReset', this.onTimelineReset);
}
onSync = async (state, prevState, data) => {
const indexManager = PlatformPeg.get().getEventIndexingManager();
if (prevState === null && state === "PREPARED") {
// Load our stored checkpoints, if any.
this.crawlerCheckpoints = await indexManager.loadCheckpoints();
console.log("EventIndex: Loaded checkpoints",
this.crawlerCheckpoints);
return;
}
if (prevState === "PREPARED" && state === "SYNCING") {
const addInitialCheckpoints = async () => {
const client = MatrixClientPeg.get();
const rooms = client.getRooms();
const isRoomEncrypted = (room) => {
return client.isRoomEncrypted(room.roomId);
};
// We only care to crawl the encrypted rooms, non-encrypted.
// rooms can use the search provided by the homeserver.
const encryptedRooms = rooms.filter(isRoomEncrypted);
console.log("EventIndex: Adding initial crawler checkpoints");
// Gather the prev_batch tokens and create checkpoints for
// our message crawler.
await Promise.all(encryptedRooms.map(async (room) => {
const timeline = room.getLiveTimeline();
const token = timeline.getPaginationToken("b");
console.log("EventIndex: Got token for indexer",
room.roomId, token);
const backCheckpoint = {
roomId: room.roomId,
token: token,
direction: "b",
};
const forwardCheckpoint = {
roomId: room.roomId,
token: token,
direction: "f",
};
await indexManager.addCrawlerCheckpoint(backCheckpoint);
await indexManager.addCrawlerCheckpoint(forwardCheckpoint);
this.crawlerCheckpoints.push(backCheckpoint);
this.crawlerCheckpoints.push(forwardCheckpoint);
}));
};
// If our indexer is empty we're most likely running Riot the
// first time with indexing support or running it with an
// initial sync. Add checkpoints to crawl our encrypted rooms.
const eventIndexWasEmpty = await indexManager.isEventIndexEmpty();
if (eventIndexWasEmpty) await addInitialCheckpoints();
// Start our crawler.
this.startCrawler();
return;
}
if (prevState === "SYNCING" && state === "SYNCING") {
// A sync was done, presumably we queued up some live events,
// commit them now.
console.log("EventIndex: Committing events");
await indexManager.commitLiveEvents();
return;
}
}
onRoomTimeline = async (ev, room, toStartOfTimeline, removed, data) => {
// We only index encrypted rooms locally.
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return;
// If it isn't a live event or if it's redacted there's nothing to
// do.
if (toStartOfTimeline || !data || !data.liveEvent
|| ev.isRedacted()) {
return;
}
// If the event is not yet decrypted mark it for the
// Event.decrypted callback.
if (ev.isBeingDecrypted()) {
const eventId = ev.getId();
this.liveEventsForIndex.add(eventId);
} else {
// If the event is decrypted or is unencrypted add it to the
// index now.
await this.addLiveEventToIndex(ev);
}
}
onEventDecrypted = async (ev, err) => {
const eventId = ev.getId();
// If the event isn't in our live event set, ignore it.
if (!this.liveEventsForIndex.delete(eventId)) return;
if (err) return;
await this.addLiveEventToIndex(ev);
}
async addLiveEventToIndex(ev) {
const indexManager = PlatformPeg.get().getEventIndexingManager();
if (["m.room.message", "m.room.name", "m.room.topic"]
.indexOf(ev.getType()) == -1) {
return;
}
const e = ev.toJSON().decrypted;
const profile = {
displayname: ev.sender.rawDisplayName,
avatar_url: ev.sender.getMxcAvatarUrl(),
};
indexManager.addEventToIndex(e, profile);
}
async crawlerFunc() {
// TODO either put this in a better place or find a library provided
// method that does this.
const sleep = async (ms) => {
return new Promise(resolve => setTimeout(resolve, ms));
};
let cancelled = false;
console.log("EventIndex: Started crawler function");
const client = MatrixClientPeg.get();
const indexManager = PlatformPeg.get().getEventIndexingManager();
this._crawler = {};
this._crawler.cancel = () => {
cancelled = true;
};
while (!cancelled) {
// This is a low priority task and we don't want to spam our
// homeserver with /messages requests so we set a hefty timeout
// here.
await sleep(this._crawlerTimeout);
console.log("EventIndex: Running the crawler loop.");
if (cancelled) {
break;
}
const checkpoint = this.crawlerCheckpoints.shift();
/// There is no checkpoint available currently, one may appear if
// a sync with limited room timelines happens, so go back to sleep.
if (checkpoint === undefined) {
continue;
}
console.log("EventIndex: crawling using checkpoint", checkpoint);
// We have a checkpoint, let us fetch some messages, again, very
// conservatively to not bother our homeserver too much.
const eventMapper = client.getEventMapper();
// TODO we need to ensure to use member lazy loading with this
// request so we get the correct profiles.
let res;
try {
res = await client._createMessagesRequest(
checkpoint.roomId, checkpoint.token, this._eventsPerCrawl,
checkpoint.direction);
} catch (e) {
console.log("EventIndex: Error crawling events:", e);
this.crawlerCheckpoints.push(checkpoint);
continue;
}
if (res.chunk.length === 0) {
console.log("EventIndex: Done with the checkpoint", checkpoint);
// We got to the start/end of our timeline, lets just
// delete our checkpoint and go back to sleep.
await indexManager.removeCrawlerCheckpoint(checkpoint);
continue;
}
// Convert the plain JSON events into Matrix events so they get
// decrypted if necessary.
const matrixEvents = res.chunk.map(eventMapper);
let stateEvents = [];
if (res.state !== undefined) {
stateEvents = res.state.map(eventMapper);
}
const profiles = {};
stateEvents.forEach(ev => {
if (ev.event.content &&
ev.event.content.membership === "join") {
profiles[ev.event.sender] = {
displayname: ev.event.content.displayname,
avatar_url: ev.event.content.avatar_url,
};
}
});
const decryptionPromises = [];
matrixEvents.forEach(ev => {
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) {
// TODO the decryption promise is a private property, this
// should either be made public or we should convert the
// event that gets fired when decryption is done into a
// promise using the once event emitter method:
// https://nodejs.org/api/events.html#events_events_once_emitter_name
decryptionPromises.push(ev._decryptionPromise);
}
});
// Let us wait for all the events to get decrypted.
await Promise.all(decryptionPromises);
// We filter out events for which decryption failed, are redacted
// or aren't of a type that we know how to index.
const isValidEvent = (value) => {
return ([
"m.room.message",
"m.room.name",
"m.room.topic",
].indexOf(value.getType()) >= 0
&& !value.isRedacted() && !value.isDecryptionFailure()
);
// TODO do we need to check if the event has all the valid
// attributes?
};
// TODO if there are no events at this point we're missing a lot
// decryption keys, do we want to retry this checkpoint at a later
// stage?
const filteredEvents = matrixEvents.filter(isValidEvent);
// Let us convert the events back into a format that EventIndex can
// consume.
const events = filteredEvents.map((ev) => {
const jsonEvent = ev.toJSON();
let e;
if (ev.isEncrypted()) e = jsonEvent.decrypted;
else e = jsonEvent;
let profile = {};
if (e.sender in profiles) profile = profiles[e.sender];
const object = {
event: e,
profile: profile,
};
return object;
});
// Create a new checkpoint so we can continue crawling the room for
// messages.
const newCheckpoint = {
roomId: checkpoint.roomId,
token: res.end,
fullCrawl: checkpoint.fullCrawl,
direction: checkpoint.direction,
};
console.log(
"EventIndex: Crawled room",
client.getRoom(checkpoint.roomId).name,
"and fetched", events.length, "events.",
);
try {
const eventsAlreadyAdded = await indexManager.addHistoricEvents(
events, newCheckpoint, checkpoint);
// If all events were already indexed we assume that we catched
// up with our index and don't need to crawl the room further.
// Let us delete the checkpoint in that case, otherwise push
// the new checkpoint to be used by the crawler.
if (eventsAlreadyAdded === true && newCheckpoint.fullCrawl !== true) {
console.log("EventIndex: Checkpoint had already all events",
"added, stopping the crawl", checkpoint);
await indexManager.removeCrawlerCheckpoint(newCheckpoint);
} else {
this.crawlerCheckpoints.push(newCheckpoint);
}
} catch (e) {
console.log("EventIndex: Error durring a crawl", e);
// An error occurred, put the checkpoint back so we
// can retry.
this.crawlerCheckpoints.push(checkpoint);
}
}
this._crawler = null;
console.log("EventIndex: Stopping crawler function");
}
onTimelineReset = async (room, timelineSet, resetAllTimelines) => {
if (room === null) return;
const indexManager = PlatformPeg.get().getEventIndexingManager();
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return;
const timeline = room.getLiveTimeline();
const token = timeline.getPaginationToken("b");
const backwardsCheckpoint = {
roomId: room.roomId,
token: token,
fullCrawl: false,
direction: "b",
};
console.log("EventIndex: Added checkpoint because of a limited timeline",
backwardsCheckpoint);
await indexManager.addCrawlerCheckpoint(backwardsCheckpoint);
this.crawlerCheckpoints.push(backwardsCheckpoint);
}
startCrawler() {
if (this._crawler !== null) return;
this.crawlerFunc();
}
stopCrawler() {
if (this._crawler === null) return;
this._crawler.cancel();
}
async close() {
const indexManager = PlatformPeg.get().getEventIndexingManager();
this.removeListeners();
this.stopCrawler();
return indexManager.closeEventIndex();
}
async search(searchArgs) {
const indexManager = PlatformPeg.get().getEventIndexingManager();
return indexManager.searchEventIndex(searchArgs);
}
}

View file

@ -0,0 +1,115 @@
/*
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.
*/
/*
* Object holding the global EventIndex object. Can only be initialized if the
* platform supports event indexing.
*/
import PlatformPeg from "../PlatformPeg";
import EventIndex from "../indexing/EventIndex";
import SettingsStore from '../settings/SettingsStore';
class EventIndexPeg {
constructor() {
this.index = null;
}
/**
* Create a new EventIndex and initialize it if the platform supports it.
*
* @return {Promise<bool>} A promise that will resolve to true if an
* EventIndex was successfully initialized, false otherwise.
*/
async init() {
if (!SettingsStore.isFeatureEnabled("feature_event_indexing")) {
return false;
}
const indexManager = PlatformPeg.get().getEventIndexingManager();
if (!indexManager || await indexManager.supportsEventIndexing() !== true) {
console.log("EventIndex: Platform doesn't support event indexing,",
"not initializing.");
return false;
}
const index = new EventIndex();
try {
await index.init();
} catch (e) {
console.log("EventIndex: Error initializing the event index", e);
return false;
}
console.log("EventIndex: Successfully initialized the event index");
this.index = index;
return true;
}
/**
* Get the current event index.
*
* @return {EventIndex} The current event index.
*/
get() {
return this.index;
}
stop() {
if (this.index === null) return;
this.index.stopCrawler();
}
/**
* Unset our event store
*
* After a call to this the init() method will need to be called again.
*
* @return {Promise} A promise that will resolve once the event index is
* closed.
*/
async unset() {
if (this.index === null) return;
this.index.close();
this.index = null;
}
/**
* Delete our event indexer.
*
* After a call to this the init() method will need to be called again.
*
* @return {Promise} A promise that will resolve once the event index is
* deleted.
*/
async deleteEventIndex() {
const indexManager = PlatformPeg.get().getEventIndexingManager();
if (indexManager !== null) {
this.unset();
console.log("EventIndex: Deleting event index.");
await indexManager.deleteEventIndex();
}
}
}
if (!global.mxEventIndexPeg) {
global.mxEventIndexPeg = new EventIndexPeg();
}
module.exports = global.mxEventIndexPeg;

View file

@ -20,6 +20,8 @@ import {dialogTermsInteractionCallback, TermsNotSignedError} from "../Terms";
import type {Room} from "matrix-js-sdk";
import Modal from '../Modal';
import url from 'url';
import SettingsStore from "../settings/SettingsStore";
import {IntegrationManagers} from "./IntegrationManagers";
export const KIND_ACCOUNT = "account";
export const KIND_CONFIG = "config";
@ -57,19 +59,23 @@ export class IntegrationManagerInstance {
}
async open(room: Room = null, screen: string = null, integrationId: string = null): void {
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
if (!SettingsStore.getValue("integrationProvisioning")) {
return IntegrationManagers.sharedInstance().showDisabledDialog();
}
const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager");
const dialog = Modal.createTrackedDialog(
'Integration Manager', '', IntegrationsManager,
{loading: true}, 'mx_IntegrationsManager',
'Integration Manager', '', IntegrationManager,
{loading: true}, 'mx_IntegrationManager',
);
const client = this.getScalarClient();
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',
);
});
@ -94,8 +100,8 @@ export class IntegrationManagerInstance {
// Close the old dialog and open a new one
dialog.close();
Modal.createTrackedDialog(
'Integration Manager', '', IntegrationsManager,
newProps, 'mx_IntegrationsManager',
'Integration Manager', '', IntegrationManager,
newProps, 'mx_IntegrationManager',
);
}
}

View file

@ -22,6 +22,7 @@ import type {MatrixClient, MatrixEvent, Room} from "matrix-js-sdk";
import WidgetUtils from "../utils/WidgetUtils";
import MatrixClientPeg from "../MatrixClientPeg";
import {AutoDiscovery} from "matrix-js-sdk";
import SettingsStore from "../settings/SettingsStore";
const HS_MANAGERS_REFRESH_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours
const KIND_PREFERENCE = [
@ -172,15 +173,19 @@ export class IntegrationManagers {
}
openNoManagerDialog(): void {
// TODO: Is it Integrations (plural) or Integration (singular). Singular is easier spoken.
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
Modal.createTrackedDialog(
"Integration Manager", "None", IntegrationsManager,
{configured: false}, 'mx_IntegrationsManager',
);
const IntegrationsImpossibleDialog = sdk.getComponent("dialogs.IntegrationsImpossibleDialog");
Modal.createTrackedDialog('Integrations impossible', '', IntegrationsImpossibleDialog);
}
openAll(room: Room = null, screen: string = null, integrationId: string = null): void {
if (!SettingsStore.getValue("integrationProvisioning")) {
return this.showDisabledDialog();
}
if (this._managers.length === 0) {
return this.openNoManagerDialog();
}
const TabbedIntegrationManagerDialog = sdk.getComponent("views.dialogs.TabbedIntegrationManagerDialog");
Modal.createTrackedDialog(
'Tabbed Integration Manager', '', TabbedIntegrationManagerDialog,
@ -188,6 +193,11 @@ export class IntegrationManagers {
);
}
showDisabledDialog(): void {
const IntegrationsDisabledDialog = sdk.getComponent("dialogs.IntegrationsDisabledDialog");
Modal.createTrackedDialog('Integrations disabled', '', IntegrationsDisabledDialog);
}
async overwriteManagerOnAccount(manager: IntegrationManagerInstance) {
// TODO: TravisR - We should be logging out of scalar clients.
await WidgetUtils.removeIntegrationManagerWidgets();

View file

@ -19,7 +19,6 @@ limitations under the License.
import request from 'browser-request';
import counterpart from 'counterpart';
import Promise from 'bluebird';
import React from 'react';
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";

View file

@ -16,8 +16,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
// This module contains all the code needed to log the console, persist it to
// disk and submit bug reports. Rationale is as follows:
// - Monkey-patching the console is preferable to having a log library because

View file

@ -17,7 +17,6 @@ limitations under the License.
*/
import pako from 'pako';
import Promise from 'bluebird';
import MatrixClientPeg from '../MatrixClientPeg';
import PlatformPeg from '../PlatformPeg';

View file

@ -120,12 +120,6 @@ export const SETTINGS = {
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_user_info_panel": {
isFeature: true,
displayName: _td("Use the new, consistent UserInfo panel for Room Members and Group Members"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_mjolnir": {
isFeature: true,
displayName: _td("Try out new ways to ignore people (experimental)"),
@ -142,7 +136,8 @@ export const SETTINGS = {
},
"feature_dm_verification": {
isFeature: true,
displayName: _td("Send verification requests in direct message"),
displayName: _td("Send verification requests in direct message," +
" including a new verification UX in the member panel."),
supportedLevels: LEVELS_FEATURE,
default: false,
},
@ -153,6 +148,12 @@ export const SETTINGS = {
default: false,
controller: new ReloadOnChangeController(),
},
"feature_event_indexing": {
isFeature: true,
supportedLevels: LEVELS_FEATURE,
displayName: _td("Enable local event indexing and E2EE search (requires restart)"),
default: false,
},
"useCiderComposer": {
displayName: _td("Use the new, faster, composer for writing messages"),
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
@ -280,6 +281,11 @@ export const SETTINGS = {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: [],
},
"use_system_theme": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: true,
displayName: _td("Match system dark mode setting"),
},
"webRtcAllowPeerToPeer": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
displayName: _td('Allow Peer-to-Peer for 1:1 calls'),

View file

@ -126,6 +126,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
if (!content || !content['recent_rooms']) {
content = this._getSettings(BREADCRUMBS_LEGACY_EVENT_TYPE);
}
if (!content) content = {}; // If we still don't have content, make some
content['recent_rooms'] = newValue;
return MatrixClientPeg.get().setAccountData(BREADCRUMBS_EVENT_TYPE, content);

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 SettingsHandler from "./SettingsHandler";
import MatrixClientPeg from "../../MatrixClientPeg";
import {SettingLevel} from "../SettingsStore";

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 SettingsHandler from "./SettingsHandler";
/**

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 SettingsHandler from "./SettingsHandler";
import {SettingLevel} from "../SettingsStore";

View file

@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from "bluebird";
/**
* Represents the base class for all level handlers. This class performs no logic
* and should be overridden.

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import EventEmitter from 'events';
import Promise from 'bluebird';
const BULK_REQUEST_DEBOUNCE_MS = 200;

View file

@ -234,7 +234,7 @@ class RoomViewStore extends Store {
});
MatrixClientPeg.get().joinRoom(
this._state.roomAlias || this._state.roomId, payload.opts,
).done(() => {
).then(() => {
// We don't actually need to do anything here: we do *not*
// clear the 'joining' flag because the Room object and/or
// our 'joined' member event may not have come down the sync

View file

@ -19,8 +19,75 @@ import {_t} from "./languageHandler";
export const DEFAULT_THEME = "light";
import Tinter from "./Tinter";
import dis from "./dispatcher";
import SettingsStore from "./settings/SettingsStore";
export class ThemeWatcher {
static _instance = null;
constructor() {
this._themeWatchRef = null;
this._systemThemeWatchRef = null;
this._dispatcherRef = null;
// we have both here as each may either match or not match, so by having both
// we can get the tristate of dark/light/unsupported
this._preferDark = global.matchMedia("(prefers-color-scheme: dark)");
this._preferLight = global.matchMedia("(prefers-color-scheme: light)");
this._currentTheme = this.getEffectiveTheme();
}
start() {
this._themeWatchRef = SettingsStore.watchSetting("theme", null, this._onChange);
this._systemThemeWatchRef = SettingsStore.watchSetting("use_system_theme", null, this._onChange);
this._preferDark.addEventListener('change', this._onChange);
this._preferLight.addEventListener('change', this._onChange);
this._dispatcherRef = dis.register(this._onAction);
}
stop() {
this._preferDark.removeEventListener('change', this._onChange);
this._preferLight.removeEventListener('change', this._onChange);
SettingsStore.unwatchSetting(this._systemThemeWatchRef);
SettingsStore.unwatchSetting(this._themeWatchRef);
dis.unregister(this._dispatcherRef);
}
_onChange = () => {
this.recheck();
}
_onAction = (payload) => {
if (payload.action === 'recheck_theme') {
// XXX forceTheme
this.recheck(payload.forceTheme);
}
}
// XXX: forceTheme param aded here as local echo appears to be unreliable
// https://github.com/vector-im/riot-web/issues/11443
recheck(forceTheme) {
const oldTheme = this._currentTheme;
this._currentTheme = forceTheme === undefined ? this.getEffectiveTheme() : forceTheme;
if (oldTheme !== this._currentTheme) {
setTheme(this._currentTheme);
}
}
getEffectiveTheme() {
if (SettingsStore.getValue('use_system_theme')) {
if (this._preferDark.matches) return 'dark';
if (this._preferLight.matches) return 'light';
}
return SettingsStore.getValue('theme');
}
isSystemThemeSupported() {
return this._preferDark.matches || this._preferLight.matches;
}
}
export function enumerateThemes() {
const BUILTIN_THEMES = {
"light": _t("Light theme"),
@ -60,30 +127,17 @@ function getCustomTheme(themeName) {
return customTheme;
}
/**
* Gets the underlying theme name for the given theme. This is usually the theme or
* CSS resource that the theme relies upon to load.
* @param {string} theme The theme name to get the base of.
* @returns {string} The base theme (typically "light" or "dark").
*/
export function getBaseTheme(theme) {
if (!theme) return "light";
if (theme.startsWith("custom-")) {
const customTheme = getCustomTheme(theme.substr(7));
return customTheme.is_dark ? "dark-custom" : "light-custom";
}
return theme; // it's probably a base theme
}
/**
* Called whenever someone changes the theme
* Async function that returns once the theme has been set
* (ie. the CSS has been loaded)
*
* @param {string} theme new theme
*/
export function setTheme(theme) {
export async function setTheme(theme) {
if (!theme) {
theme = SettingsStore.getValue("theme");
const themeWatcher = new ThemeWatcher();
theme = themeWatcher.getEffectiveTheme();
}
let stylesheetName = theme;
if (theme.startsWith("custom-")) {
@ -122,38 +176,41 @@ export function setTheme(theme) {
styleElements[stylesheetName].disabled = false;
const switchTheme = function() {
// we re-enable our theme here just in case we raced with another
// theme set request as per https://github.com/vector-im/riot-web/issues/5601.
// We could alternatively lock or similar to stop the race, but
// this is probably good enough for now.
styleElements[stylesheetName].disabled = false;
Object.values(styleElements).forEach((a) => {
if (a == styleElements[stylesheetName]) return;
a.disabled = true;
});
Tinter.setTheme(theme);
};
return new Promise((resolve) => {
const switchTheme = function() {
// we re-enable our theme here just in case we raced with another
// theme set request as per https://github.com/vector-im/riot-web/issues/5601.
// We could alternatively lock or similar to stop the race, but
// this is probably good enough for now.
styleElements[stylesheetName].disabled = false;
Object.values(styleElements).forEach((a) => {
if (a == styleElements[stylesheetName]) return;
a.disabled = true;
});
Tinter.setTheme(theme);
resolve();
};
// turns out that Firefox preloads the CSS for link elements with
// the disabled attribute, but Chrome doesn't.
// turns out that Firefox preloads the CSS for link elements with
// the disabled attribute, but Chrome doesn't.
let cssLoaded = false;
let cssLoaded = false;
styleElements[stylesheetName].onload = () => {
switchTheme();
};
styleElements[stylesheetName].onload = () => {
switchTheme();
};
for (let i = 0; i < document.styleSheets.length; i++) {
const ss = document.styleSheets[i];
if (ss && ss.href === styleElements[stylesheetName].href) {
cssLoaded = true;
break;
for (let i = 0; i < document.styleSheets.length; i++) {
const ss = document.styleSheets[i];
if (ss && ss.href === styleElements[stylesheetName].href) {
cssLoaded = true;
break;
}
}
}
if (cssLoaded) {
styleElements[stylesheetName].onload = undefined;
switchTheme();
}
if (cssLoaded) {
styleElements[stylesheetName].onload = undefined;
switchTheme();
}
});
}

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