Merge remote-tracking branch 'origin/develop' into develop
This commit is contained in:
commit
f28885ff88
154 changed files with 3138 additions and 5177 deletions
|
@ -19,7 +19,7 @@ import MatrixClientPeg from './MatrixClientPeg';
|
|||
import { _t } from './languageHandler';
|
||||
|
||||
/**
|
||||
* Allows a user to add a third party identifier to their Home Server and,
|
||||
* Allows a user to add a third party identifier to their homeserver and,
|
||||
* optionally, the identity servers.
|
||||
*
|
||||
* This involves getting an email token from the identity server to "prove" that
|
||||
|
|
|
@ -267,7 +267,7 @@ class Analytics {
|
|||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||
Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, {
|
||||
title: _t('Analytics'),
|
||||
description: <div className="mx_UserSettings_analyticsModal">
|
||||
description: <div className="mx_AnalyticsModal">
|
||||
<div>
|
||||
{ _t('The information being sent to us to help make Riot.im better includes:') }
|
||||
</div>
|
||||
|
|
|
@ -51,7 +51,7 @@ module.exports = {
|
|||
},
|
||||
|
||||
defaultAvatarUrlForString: function(s) {
|
||||
const images = ['76cfa6', '50e2c2', 'f4c371'];
|
||||
const images = ['03b381', '368bd6', 'ac3ba8'];
|
||||
let total = 0;
|
||||
for (let i = 0; i < s.length; ++i) {
|
||||
total += s.charCodeAt(i);
|
||||
|
|
|
@ -359,7 +359,7 @@ class ContentMessages {
|
|||
if (!upload.canceled) {
|
||||
let desc = _t('The file \'%(fileName)s\' failed to upload', {fileName: upload.fileName}) + '.';
|
||||
if (err.http_status == 413) {
|
||||
desc = _t('The file \'%(fileName)s\' exceeds this home server\'s size limit for uploads', {fileName: upload.fileName});
|
||||
desc = _t('The file \'%(fileName)s\' exceeds this homeserver\'s size limit for uploads', {fileName: upload.fileName});
|
||||
}
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
|
||||
|
|
|
@ -19,16 +19,21 @@ limitations under the License.
|
|||
|
||||
import ReplyThread from "./components/views/elements/ReplyThread";
|
||||
|
||||
const React = require('react');
|
||||
const sanitizeHtml = require('sanitize-html');
|
||||
const highlight = require('highlight.js');
|
||||
const linkifyMatrix = require('./linkify-matrix');
|
||||
import React from 'react';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import highlight from 'highlight.js';
|
||||
import * as linkify from 'linkifyjs';
|
||||
import linkifyMatrix from './linkify-matrix';
|
||||
import _linkifyElement from 'linkifyjs/element';
|
||||
import _linkifyString from 'linkifyjs/string';
|
||||
import escape from 'lodash/escape';
|
||||
import emojione from 'emojione';
|
||||
import classNames from 'classnames';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import url from 'url';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
emojione.imagePathSVG = 'emojione/svg/';
|
||||
// Store PNG path for displaying many flags at once (for increased performance over SVG)
|
||||
emojione.imagePathPNG = 'emojione/png/';
|
||||
|
@ -63,8 +68,10 @@ export function containsEmoji(str) {
|
|||
/* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js
|
||||
* because we want to include emoji shortnames in title text
|
||||
*/
|
||||
function unicodeToImage(str) {
|
||||
let replaceWith; let unicode; let alt; let short; let fname;
|
||||
function unicodeToImage(str, addAlt) {
|
||||
if (addAlt === undefined) addAlt = true;
|
||||
|
||||
let replaceWith; let unicode; let short; let fname;
|
||||
const mappedUnicode = emojione.mapUnicodeToShort();
|
||||
|
||||
str = str.replace(emojione.regUnicode, function(unicodeChar) {
|
||||
|
@ -79,10 +86,14 @@ function unicodeToImage(str) {
|
|||
fname = emojione.emojioneList[short].fname;
|
||||
|
||||
// depending on the settings, we'll either add the native unicode as the alt tag, otherwise the shortname
|
||||
alt = (emojione.unicodeAlt) ? emojione.convert(unicode.toUpperCase()) : mappedUnicode[unicode];
|
||||
const title = mappedUnicode[unicode];
|
||||
|
||||
replaceWith = `<img class="mx_emojione" title="${title}" alt="${alt}" src="${emojione.imagePathSVG}${fname}.svg${emojione.cacheBustParam}"/>`;
|
||||
if (addAlt) {
|
||||
const alt = (emojione.unicodeAlt) ? emojione.convert(unicode.toUpperCase()) : mappedUnicode[unicode];
|
||||
replaceWith = `<img class="mx_emojione" title="${title}" alt="${alt}" src="${emojione.imagePathSVG}${fname}.svg${emojione.cacheBustParam}"/>`;
|
||||
} else {
|
||||
replaceWith = `<img class="mx_emojione" src="${emojione.imagePathSVG}${fname}.svg${emojione.cacheBustParam}"/>`;
|
||||
}
|
||||
return replaceWith;
|
||||
}
|
||||
});
|
||||
|
@ -503,8 +514,39 @@ export function bodyToHtml(content, highlights, opts={}) {
|
|||
<span className={className} dir="auto">{ strippedBody }</span>;
|
||||
}
|
||||
|
||||
export function emojifyText(text) {
|
||||
export function emojifyText(text, addAlt) {
|
||||
return {
|
||||
__html: unicodeToImage(escape(text)),
|
||||
__html: unicodeToImage(escape(text), addAlt),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Linkifies the given string. This is a wrapper around 'linkifyjs/string'.
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
export function linkifyString(str) {
|
||||
return _linkifyString(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Linkifies the given DOM element. This is a wrapper around 'linkifyjs/element'.
|
||||
*
|
||||
* @param {object} element DOM element to linkify
|
||||
* @param {object} [options] Options for linkifyElement. Default: linkifyMatrix.options
|
||||
* @returns {object}
|
||||
*/
|
||||
export function linkifyElement(element, options = linkifyMatrix.options) {
|
||||
return _linkifyElement(element, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Linkify the given string and sanitize the HTML afterwards.
|
||||
*
|
||||
* @param {string} dirtyHtml The HTML string to sanitize and linkify
|
||||
* @returns {string}
|
||||
*/
|
||||
export function linkifyAndSanitizeHtml(dirtyHtml) {
|
||||
return sanitizeHtml(linkifyString(dirtyHtml), sanitizeHtmlParams);
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ class MatrixClientPeg {
|
|||
|
||||
/**
|
||||
* Replace this MatrixClientPeg's client with a client instance that has
|
||||
* Home Server / Identity Server URLs and active credentials
|
||||
* homeserver / identity server URLs and active credentials
|
||||
*/
|
||||
replaceUsingCreds(creds: MatrixClientCreds) {
|
||||
this._currentClientCreds = creds;
|
||||
|
@ -135,14 +135,7 @@ class MatrixClientPeg {
|
|||
const opts = utils.deepCopy(this.opts);
|
||||
// the react sdk doesn't work without this, so don't allow
|
||||
opts.pendingEventOrdering = "detached";
|
||||
|
||||
const LAZY_LOADING_FEATURE = "feature_lazyloading";
|
||||
if (SettingsStore.isFeatureEnabled(LAZY_LOADING_FEATURE)) {
|
||||
const userId = this.matrixClient.credentials.userId;
|
||||
if (phasedRollOutExpiredForUser(userId, LAZY_LOADING_FEATURE, Date.now())) {
|
||||
opts.lazyLoadMembers = true;
|
||||
}
|
||||
}
|
||||
opts.lazyLoadMembers = true;
|
||||
|
||||
// Connect the matrix client to the dispatcher
|
||||
MatrixActionCreators.start(this.matrixClient);
|
||||
|
@ -164,14 +157,14 @@ class MatrixClientPeg {
|
|||
}
|
||||
|
||||
/**
|
||||
* Return the server name of the user's home server
|
||||
* Throws an error if unable to deduce the home server name
|
||||
* Return the server name of the user's homeserver
|
||||
* Throws an error if unable to deduce the homeserver name
|
||||
* (eg. if the user is not logged in)
|
||||
*/
|
||||
getHomeServerName() {
|
||||
const matches = /^@.+:(.+)$/.exec(this.matrixClient.credentials.userId);
|
||||
if (matches === null || matches.length < 1) {
|
||||
throw new Error("Failed to derive home server name from user ID!");
|
||||
throw new Error("Failed to derive homeserver name from user ID!");
|
||||
}
|
||||
return matches[1];
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ limitations under the License.
|
|||
export default {
|
||||
HomePage: "home_page",
|
||||
RoomView: "room_view",
|
||||
UserSettings: "user_settings",
|
||||
RoomDirectory: "room_directory",
|
||||
UserView: "user_view",
|
||||
GroupView: "group_view",
|
||||
|
|
|
@ -33,7 +33,7 @@ class Presence {
|
|||
}
|
||||
/**
|
||||
* Start listening the user activity to evaluate his presence state.
|
||||
* Any state change will be sent to the Home Server.
|
||||
* Any state change will be sent to the homeserver.
|
||||
*/
|
||||
async start() {
|
||||
this._unavailableTimer = new Timer(UNAVAILABLE_TIME_MS);
|
||||
|
@ -78,7 +78,7 @@ class Presence {
|
|||
|
||||
/**
|
||||
* Set the presence state.
|
||||
* If the state has changed, the Home Server will be notified.
|
||||
* If the state has changed, the homeserver will be notified.
|
||||
* @param {string} newState the new presence state (see PRESENCE enum)
|
||||
*/
|
||||
async setState(newState) {
|
||||
|
|
|
@ -35,8 +35,10 @@ export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/;
|
|||
* on what the HS supports
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {bool} options.go_home_on_cancel If true, goes to
|
||||
* the hame page if the user cancels the action
|
||||
* @param {bool} options.go_home_on_cancel
|
||||
* If true, goes to the home page if the user cancels the action
|
||||
* @param {bool} options.go_welcome_on_cancel
|
||||
* If true, goes to the welcome page if the user cancels the action
|
||||
*/
|
||||
export async function startAnyRegistrationFlow(options) {
|
||||
if (options === undefined) options = {};
|
||||
|
@ -73,6 +75,8 @@ export async function startAnyRegistrationFlow(options) {
|
|||
dis.dispatch({action: 'start_registration'});
|
||||
} else if (options.go_home_on_cancel) {
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
} else if (options.go_welcome_on_cancel) {
|
||||
dis.dispatch({action: 'view_welcome_page'});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -23,6 +23,47 @@ export const ALL_MESSAGES = 'all_messages';
|
|||
export const MENTIONS_ONLY = 'mentions_only';
|
||||
export const MUTE = 'mute';
|
||||
|
||||
|
||||
function _shouldShowNotifBadge(roomNotifState) {
|
||||
const showBadgeInStates = [ALL_MESSAGES, ALL_MESSAGES_LOUD];
|
||||
return showBadgeInStates.indexOf(roomNotifState) > -1;
|
||||
}
|
||||
|
||||
function _shouldShowMentionBadge(roomNotifState) {
|
||||
return roomNotifState !== MUTE;
|
||||
}
|
||||
|
||||
export function aggregateNotificationCount(rooms) {
|
||||
return rooms.reduce((result, room, index) => {
|
||||
const roomNotifState = getRoomNotifsState(room.roomId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const notificationCount = room.getUnreadNotificationCount();
|
||||
|
||||
const notifBadges = notificationCount > 0 && _shouldShowNotifBadge(roomNotifState);
|
||||
const mentionBadges = highlight && _shouldShowMentionBadge(roomNotifState);
|
||||
const badges = notifBadges || mentionBadges;
|
||||
|
||||
if (badges) {
|
||||
result.count += notificationCount;
|
||||
if (highlight) {
|
||||
result.highlight = true;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, {count: 0, highlight: false});
|
||||
}
|
||||
|
||||
export function getRoomHasBadge(room) {
|
||||
const roomNotifState = getRoomNotifsState(room.roomId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const notificationCount = room.getUnreadNotificationCount();
|
||||
|
||||
const notifBadges = notificationCount > 0 && _shouldShowNotifBadge(roomNotifState);
|
||||
const mentionBadges = highlight && _shouldShowMentionBadge(roomNotifState);
|
||||
|
||||
return notifBadges || mentionBadges;
|
||||
}
|
||||
|
||||
export function getRoomNotifsState(roomId) {
|
||||
if (MatrixClientPeg.get().isGuest()) return ALL_MESSAGES;
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ import SettingsStore, {SettingLevel} from './settings/SettingsStore';
|
|||
import {MATRIXTO_URL_PATTERN} from "./linkify-matrix";
|
||||
import * as querystring from "querystring";
|
||||
import MultiInviter from './utils/MultiInviter';
|
||||
|
||||
import { linkifyAndSanitizeHtml } from './HtmlUtils';
|
||||
|
||||
class Command {
|
||||
constructor({name, args='', description, runFn, hideCompletionAfterSpace=false}) {
|
||||
|
@ -137,13 +137,26 @@ export const CommandMap = {
|
|||
|
||||
topic: new Command({
|
||||
name: 'topic',
|
||||
args: '<topic>',
|
||||
description: _td('Sets the room topic'),
|
||||
args: '[<topic>]',
|
||||
description: _td('Gets or sets the room topic'),
|
||||
runFn: function(roomId, args) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (args) {
|
||||
return success(MatrixClientPeg.get().setRoomTopic(roomId, args));
|
||||
return success(cli.setRoomTopic(roomId, args));
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
const room = cli.getRoom(roomId);
|
||||
if (!room) return reject('Bad room ID: ' + roomId);
|
||||
|
||||
const topicEvents = room.currentState.getStateEvents('m.room.topic', '');
|
||||
const topic = topicEvents && topicEvents.getContent().topic;
|
||||
const topicHtml = topic ? linkifyAndSanitizeHtml(topic) : _t('This room has no topic.');
|
||||
|
||||
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
|
||||
Modal.createTrackedDialog('Slash Commands', 'Topic', InfoDialog, {
|
||||
title: room.name,
|
||||
description: <div dangerouslySetInnerHTML={{ __html: topicHtml }} />,
|
||||
});
|
||||
return success();
|
||||
},
|
||||
}),
|
||||
|
||||
|
@ -391,13 +404,12 @@ export const CommandMap = {
|
|||
ignoredUsers.push(userId); // de-duped internally in the js-sdk
|
||||
return success(
|
||||
cli.setIgnoredUsers(ignoredUsers).then(() => {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, {
|
||||
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
|
||||
Modal.createTrackedDialog('Slash Commands', 'User ignored', InfoDialog, {
|
||||
title: _t('Ignored user'),
|
||||
description: <div>
|
||||
<p>{ _t('You are now ignoring %(userId)s', {userId}) }</p>
|
||||
</div>,
|
||||
hasCancelButton: false,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
@ -423,13 +435,12 @@ export const CommandMap = {
|
|||
if (index !== -1) ignoredUsers.splice(index, 1);
|
||||
return success(
|
||||
cli.setIgnoredUsers(ignoredUsers).then(() => {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, {
|
||||
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
|
||||
Modal.createTrackedDialog('Slash Commands', 'User unignored', InfoDialog, {
|
||||
title: _t('Unignored user'),
|
||||
description: <div>
|
||||
<p>{ _t('You are no longer ignoring %(userId)s', {userId}) }</p>
|
||||
</div>,
|
||||
hasCancelButton: false,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
@ -546,8 +557,8 @@ export const CommandMap = {
|
|||
return cli.setDeviceVerified(userId, deviceId, true);
|
||||
}).then(() => {
|
||||
// Tell the user we verified everything
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, {
|
||||
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
|
||||
Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, {
|
||||
title: _t('Verified key'),
|
||||
description: <div>
|
||||
<p>
|
||||
|
@ -558,7 +569,6 @@ export const CommandMap = {
|
|||
}
|
||||
</p>
|
||||
</div>,
|
||||
hasCancelButton: false,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -134,6 +134,38 @@ function textForTombstoneEvent(ev) {
|
|||
return _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName});
|
||||
}
|
||||
|
||||
function textForJoinRulesEvent(ev) {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
switch (ev.getContent().join_rule) {
|
||||
case "public":
|
||||
return _t('%(senderDisplayName)s made the room public to whoever knows the link.', {senderDisplayName});
|
||||
case "invite":
|
||||
return _t('%(senderDisplayName)s made the room invite only.', {senderDisplayName});
|
||||
default:
|
||||
// The spec supports "knock" and "private", however nothing implements these.
|
||||
return _t('%(senderDisplayName)s changed the join rule to %(rule)s', {
|
||||
senderDisplayName,
|
||||
rule: ev.getContent().join_rule,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function textForGuestAccessEvent(ev) {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
switch (ev.getContent().guest_access) {
|
||||
case "can_join":
|
||||
return _t('%(senderDisplayName)s has allowed guests to join the room.', {senderDisplayName});
|
||||
case "forbidden":
|
||||
return _t('%(senderDisplayName)s has prevented guests from joining the room.', {senderDisplayName});
|
||||
default:
|
||||
// There's no other options we can expect, however just for safety's sake we'll do this.
|
||||
return _t('%(senderDisplayName)s changed guest access to %(rule)s', {
|
||||
senderDisplayName,
|
||||
rule: ev.getContent().guest_access,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function textForServerACLEvent(ev) {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
const prevContent = ev.getPrevContent();
|
||||
|
@ -439,6 +471,8 @@ const stateHandlers = {
|
|||
'm.room.pinned_events': textForPinnedEvent,
|
||||
'm.room.server_acl': textForServerACLEvent,
|
||||
'm.room.tombstone': textForTombstoneEvent,
|
||||
'm.room.join_rules': textForJoinRulesEvent,
|
||||
'm.room.guest_access': textForGuestAccessEvent,
|
||||
|
||||
'im.vector.modular.widgets': textForWidgetEvent,
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,47 +15,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
|
||||
/*
|
||||
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
|
||||
*/
|
||||
// TODO: Decommission.
|
||||
// Ref: https://github.com/vector-im/riot-web/issues/8424
|
||||
export default {
|
||||
loadProfileInfo: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli.getProfileInfo(cli.credentials.userId);
|
||||
},
|
||||
|
||||
saveDisplayName: function(newDisplayname) {
|
||||
return MatrixClientPeg.get().setDisplayName(newDisplayname);
|
||||
},
|
||||
|
||||
loadThreePids: function() {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return Promise.resolve({
|
||||
threepids: [],
|
||||
}); // guests can't poke 3pid endpoint
|
||||
}
|
||||
return MatrixClientPeg.get().getThreePids();
|
||||
},
|
||||
|
||||
saveThreePids: function(threePids) {
|
||||
// TODO
|
||||
},
|
||||
|
||||
changePassword: function(oldPassword, newPassword) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
const authDict = {
|
||||
type: 'm.login.password',
|
||||
user: cli.credentials.userId,
|
||||
password: oldPassword,
|
||||
};
|
||||
|
||||
return cli.setPassword(authDict, newPassword);
|
||||
},
|
||||
|
||||
/*
|
||||
* Returns the email pusher (pusher of type 'email') for a given
|
||||
* email address. Email pushers all have the same app ID, so since
|
||||
|
@ -74,10 +38,6 @@ export default {
|
|||
return undefined;
|
||||
},
|
||||
|
||||
hasEmailPusher: function(pushers, address) {
|
||||
return this.getEmailPusher(pushers, address) !== undefined;
|
||||
},
|
||||
|
||||
addEmailPusher: function(address, data) {
|
||||
return MatrixClientPeg.get().setPusher({
|
||||
kind: 'email',
|
||||
|
|
125
src/components/structures/CustomRoomTagPanel.js
Normal file
125
src/components/structures/CustomRoomTagPanel.js
Normal file
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd.
|
||||
|
||||
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 CustomRoomTagStore from '../../stores/CustomRoomTagStore';
|
||||
import AutoHideScrollbar from './AutoHideScrollbar';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import classNames from 'classnames';
|
||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||
|
||||
class CustomRoomTagPanel extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tags: CustomRoomTagStore.getSortedTags(),
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._tagStoreToken = CustomRoomTagStore.addListener(() => {
|
||||
this.setState({tags: CustomRoomTagStore.getSortedTags()});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._tagStoreToken) {
|
||||
this._tagStoreToken.remove();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const tags = this.state.tags.map((tag) => {
|
||||
return (<CustomRoomTagTile tag={tag} key={tag.name} />);
|
||||
});
|
||||
|
||||
const classes = classNames('mx_CustomRoomTagPanel', {
|
||||
mx_CustomRoomTagPanel_empty: this.state.tags.length === 0,
|
||||
});
|
||||
|
||||
return (<div className={classes}>
|
||||
<div className="mx_CustomRoomTagPanel_divider" />
|
||||
<AutoHideScrollbar className="mx_CustomRoomTagPanel_scroller">
|
||||
{tags}
|
||||
</AutoHideScrollbar>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomRoomTagTile extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {hover: false};
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.onMouseOut = this.onMouseOut.bind(this);
|
||||
this.onMouseOver = this.onMouseOver.bind(this);
|
||||
}
|
||||
|
||||
onMouseOver() {
|
||||
this.setState({hover: true});
|
||||
}
|
||||
|
||||
onMouseOut() {
|
||||
this.setState({hover: false});
|
||||
}
|
||||
|
||||
onClick() {
|
||||
dis.dispatch({action: 'select_custom_room_tag', tag: this.props.tag.name});
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
|
||||
|
||||
const tag = this.props.tag;
|
||||
const avatarHeight = 40;
|
||||
const className = classNames({
|
||||
CustomRoomTagPanel_tileSelected: tag.selected,
|
||||
});
|
||||
const name = tag.name;
|
||||
const badge = tag.badge;
|
||||
let badgeElement;
|
||||
if (badge) {
|
||||
const badgeClasses = classNames({
|
||||
"mx_TagTile_badge": true,
|
||||
"mx_TagTile_badgeHighlight": badge.highlight,
|
||||
});
|
||||
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>);
|
||||
}
|
||||
|
||||
const tip = (this.state.hover ?
|
||||
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
|
||||
<div />);
|
||||
return (
|
||||
<AccessibleButton className={className} onClick={this.onClick}>
|
||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||
<BaseAvatar
|
||||
name={tag.avatarLetter}
|
||||
idName={name}
|
||||
width={avatarHeight}
|
||||
height={avatarHeight}
|
||||
/>
|
||||
{ badgeElement }
|
||||
{ tip }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomRoomTagPanel;
|
116
src/components/structures/EmbeddedPage.js
Normal file
116
src/components/structures/EmbeddedPage.js
Normal file
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import request from 'browser-request';
|
||||
import { _t } from '../../languageHandler';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import sdk from '../../index';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default class EmbeddedPage extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// URL to request embedded page content from
|
||||
url: PropTypes.string,
|
||||
// Class name prefix to apply for a given instance
|
||||
className: PropTypes.string,
|
||||
// Whether to wrap the page in a scrollbar
|
||||
scrollbar: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
page: '',
|
||||
};
|
||||
}
|
||||
|
||||
translate(s) {
|
||||
// default implementation - skins may wish to extend this
|
||||
return sanitizeHtml(_t(s));
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._unmounted = false;
|
||||
|
||||
if (!this.props.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we use request() to inline the page into the react component
|
||||
// so that it can inherit CSS and theming easily rather than mess around
|
||||
// with iframes and trying to synchronise document.stylesheets.
|
||||
|
||||
request(
|
||||
{ method: "GET", url: this.props.url },
|
||||
(err, response, body) => {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err || response.status < 200 || response.status >= 300) {
|
||||
console.warn(`Error loading page: ${err}`);
|
||||
this.setState({ page: _t("Couldn't load page") });
|
||||
return;
|
||||
}
|
||||
|
||||
body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1));
|
||||
this.setState({ page: body });
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
render() {
|
||||
const client = this.context.matrixClient;
|
||||
const isGuest = client ? client.isGuest() : true;
|
||||
const className = this.props.className;
|
||||
const classes = classnames({
|
||||
[className]: true,
|
||||
[`${className}_guest`]: isGuest,
|
||||
});
|
||||
|
||||
const content = <div className={`${className}_body`}
|
||||
dangerouslySetInnerHTML={{ __html: this.state.page }}
|
||||
>
|
||||
</div>;
|
||||
|
||||
if (this.props.scrollbar) {
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
return <GeminiScrollbarWrapper autoshow={true} className={classes}>
|
||||
{content}
|
||||
</GeminiScrollbarWrapper>;
|
||||
} else {
|
||||
return <div className={classes}>
|
||||
{content}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1324,7 +1324,7 @@ export default React.createClass({
|
|||
} else {
|
||||
let extraText;
|
||||
if (this.state.error.errcode === 'M_UNRECOGNIZED') {
|
||||
extraText = <div>{ _t('This Home server does not support communities') }</div>;
|
||||
extraText = <div>{ _t('This homeserver does not support communities') }</div>;
|
||||
}
|
||||
return (
|
||||
<div className="mx_GroupView_error">
|
||||
|
|
|
@ -1,144 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import request from 'browser-request';
|
||||
import { _t } from '../../languageHandler';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import sdk from '../../index';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import dis from '../../dispatcher';
|
||||
|
||||
class HomePage extends React.Component {
|
||||
static displayName = 'HomePage';
|
||||
|
||||
static propTypes = {
|
||||
// URL to use as the iFrame src. Defaults to /home.html.
|
||||
homePageUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
};
|
||||
|
||||
state = {
|
||||
iframeSrc: '',
|
||||
page: '',
|
||||
};
|
||||
|
||||
translate(s) {
|
||||
// default implementation - skins may wish to extend this
|
||||
return sanitizeHtml(_t(s));
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._unmounted = false;
|
||||
|
||||
// we use request() to inline the homepage into the react component
|
||||
// so that it can inherit CSS and theming easily rather than mess around
|
||||
// with iframes and trying to synchronise document.stylesheets.
|
||||
|
||||
const src = this.props.homePageUrl || 'home.html';
|
||||
|
||||
request(
|
||||
{ method: "GET", url: src },
|
||||
(err, response, body) => {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err || response.status < 200 || response.status >= 300) {
|
||||
console.warn(`Error loading home page: ${err}`);
|
||||
this.setState({ page: _t("Couldn't load home page") });
|
||||
return;
|
||||
}
|
||||
|
||||
body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1));
|
||||
this.setState({ page: body });
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
onLoginClick(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
dis.dispatch({ action: 'start_login' });
|
||||
}
|
||||
|
||||
onRegisterClick(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
dis.dispatch({ action: 'start_registration' });
|
||||
}
|
||||
|
||||
render() {
|
||||
let guestWarning = "";
|
||||
if (this.context.matrixClient.isGuest()) {
|
||||
guestWarning = (
|
||||
<div className="mx_HomePage_guest_warning">
|
||||
<img src={require("../../../res/img/warning.svg")} width="24" height="23" />
|
||||
<div>
|
||||
<div>
|
||||
{ _t("You are currently using Riot anonymously as a guest.") }
|
||||
</div>
|
||||
<div>
|
||||
{ _t(
|
||||
'If you would like to create a Matrix account you can <a>register</a> now.',
|
||||
{},
|
||||
{ 'a': (sub) => <a href="#" onClick={this.onRegisterClick}>{ sub }</a> },
|
||||
) }
|
||||
</div>
|
||||
<div>
|
||||
{ _t(
|
||||
'If you already have a Matrix account you can <a>log in</a> instead.',
|
||||
{},
|
||||
{ 'a': (sub) => <a href="#" onClick={this.onLoginClick}>{ sub }</a> },
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.iframeSrc) {
|
||||
return (
|
||||
<div className="mx_HomePage">
|
||||
{ guestWarning }
|
||||
<iframe src={ this.state.iframeSrc } />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
return (
|
||||
<GeminiScrollbarWrapper autoshow={true} className="mx_HomePage">
|
||||
{ guestWarning }
|
||||
<div className="mx_HomePage_body" dangerouslySetInnerHTML={{ __html: this.state.page }}>
|
||||
</div>
|
||||
</GeminiScrollbarWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HomePage;
|
|
@ -24,7 +24,7 @@ import { KeyCode } from '../../Keyboard';
|
|||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import VectorConferenceHandler from '../../VectorConferenceHandler';
|
||||
|
||||
import TagPanelButtons from './TagPanelButtons';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
|
||||
|
||||
|
@ -183,12 +183,23 @@ const LeftPanel = React.createClass({
|
|||
render: function() {
|
||||
const RoomList = sdk.getComponent('rooms.RoomList');
|
||||
const TagPanel = sdk.getComponent('structures.TagPanel');
|
||||
const CustomRoomTagPanel = sdk.getComponent('structures.CustomRoomTagPanel');
|
||||
const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton');
|
||||
const SearchBox = sdk.getComponent('structures.SearchBox');
|
||||
const CallPreview = sdk.getComponent('voip.CallPreview');
|
||||
|
||||
const tagPanelEnabled = SettingsStore.getValue("TagPanel.enableTagPanel");
|
||||
const tagPanel = tagPanelEnabled ? <TagPanel /> : <div />;
|
||||
let tagPanelContainer;
|
||||
|
||||
const isCustomTagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags");
|
||||
|
||||
if (tagPanelEnabled) {
|
||||
tagPanelContainer = (<div className="mx_LeftPanel_tagPanelContainer">
|
||||
<TagPanel />
|
||||
{ isCustomTagsEnabled ? <CustomRoomTagPanel /> : undefined }
|
||||
<TagPanelButtons />
|
||||
</div>);
|
||||
}
|
||||
|
||||
const containerClasses = classNames(
|
||||
"mx_LeftPanel_container", "mx_fadable",
|
||||
|
@ -199,13 +210,15 @@ const LeftPanel = React.createClass({
|
|||
},
|
||||
);
|
||||
|
||||
const searchBox = !this.props.collapsed ?
|
||||
<SearchBox onSearch={ this.onSearch } onCleared={ this.onSearchCleared } /> :
|
||||
undefined;
|
||||
const searchBox = (<SearchBox
|
||||
onSearch={ this.onSearch }
|
||||
onCleared={ this.onSearchCleared }
|
||||
collapsed={this.props.collapsed} />);
|
||||
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
{ tagPanel }
|
||||
{ tagPanelContainer }
|
||||
<aside className={"mx_LeftPanel dark-panel"} onKeyDown={ this._onKeyDown } onFocus={ this._onFocus } onBlur={ this._onBlur }>
|
||||
<TopLeftMenuButton collapsed={ this.props.collapsed } />
|
||||
{ searchBox }
|
||||
|
|
|
@ -57,7 +57,6 @@ const LoggedInView = React.createClass({
|
|||
matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||
page_type: PropTypes.string.isRequired,
|
||||
onRoomCreated: PropTypes.func,
|
||||
onUserSettingsClose: PropTypes.func,
|
||||
|
||||
// Called with the credentials of a registered user (if they were a ROU that
|
||||
// transitioned to PWLU)
|
||||
|
@ -421,8 +420,7 @@ const LoggedInView = React.createClass({
|
|||
render: function() {
|
||||
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||
const RoomView = sdk.getComponent('structures.RoomView');
|
||||
const UserSettings = sdk.getComponent('structures.UserSettings');
|
||||
const HomePage = sdk.getComponent('structures.HomePage');
|
||||
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
|
||||
const GroupView = sdk.getComponent('structures.GroupView');
|
||||
const MyGroups = sdk.getComponent('structures.MyGroups');
|
||||
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||
|
@ -451,13 +449,6 @@ const LoggedInView = React.createClass({
|
|||
/>;
|
||||
break;
|
||||
|
||||
case PageTypes.UserSettings:
|
||||
pageElement = <UserSettings
|
||||
onClose={this.props.onCloseAllSettings}
|
||||
brand={this.props.config.brand}
|
||||
/>;
|
||||
break;
|
||||
|
||||
case PageTypes.MyGroups:
|
||||
pageElement = <MyGroups />;
|
||||
break;
|
||||
|
@ -468,8 +459,20 @@ const LoggedInView = React.createClass({
|
|||
|
||||
case PageTypes.HomePage:
|
||||
{
|
||||
pageElement = <HomePage
|
||||
homePageUrl={this.props.config.welcomePageUrl}
|
||||
const pagesConfig = this.props.config.embeddedPages;
|
||||
let pageUrl = null;
|
||||
if (pagesConfig) {
|
||||
pageUrl = pagesConfig.homeUrl;
|
||||
}
|
||||
if (!pageUrl) {
|
||||
// This is a deprecated config option for the home page
|
||||
// (despite the name, given we also now have a welcome
|
||||
// page, which is not the same).
|
||||
pageUrl = this.props.config.welcomePageUrl;
|
||||
}
|
||||
pageElement = <EmbeddedPage className="mx_HomePage"
|
||||
url={pageUrl}
|
||||
scrollbar={true}
|
||||
/>;
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -60,27 +60,30 @@ const VIEWS = {
|
|||
// trying to re-animate a matrix client or register as a guest.
|
||||
LOADING: 0,
|
||||
|
||||
// we are showing the welcome view
|
||||
WELCOME: 1,
|
||||
|
||||
// we are showing the login view
|
||||
LOGIN: 1,
|
||||
LOGIN: 2,
|
||||
|
||||
// we are showing the registration view
|
||||
REGISTER: 2,
|
||||
REGISTER: 3,
|
||||
|
||||
// completeing the registration flow
|
||||
POST_REGISTRATION: 3,
|
||||
POST_REGISTRATION: 4,
|
||||
|
||||
// showing the 'forgot password' view
|
||||
FORGOT_PASSWORD: 4,
|
||||
FORGOT_PASSWORD: 5,
|
||||
|
||||
// we have valid matrix credentials (either via an explicit login, via the
|
||||
// initial re-animation/guest registration, or via a registration), and are
|
||||
// now setting up a matrixclient to talk to it. This isn't an instant
|
||||
// process because we need to clear out indexeddb. While it is going on we
|
||||
// show a big spinner.
|
||||
LOGGING_IN: 5,
|
||||
LOGGING_IN: 6,
|
||||
|
||||
// we are logged in with an active matrix client.
|
||||
LOGGED_IN: 6,
|
||||
LOGGED_IN: 7,
|
||||
};
|
||||
|
||||
// Actions that are redirected through the onboarding process prior to being
|
||||
|
@ -136,10 +139,6 @@ export default React.createClass({
|
|||
appConfig: PropTypes.object,
|
||||
},
|
||||
|
||||
AuxPanel: {
|
||||
RoomSettings: "room_settings",
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
return {
|
||||
appConfig: this.props.config,
|
||||
|
@ -358,8 +357,8 @@ export default React.createClass({
|
|||
});
|
||||
}).then((loadedSession) => {
|
||||
if (!loadedSession) {
|
||||
// fall back to showing the login screen
|
||||
dis.dispatch({action: "start_login"});
|
||||
// fall back to showing the welcome screen
|
||||
dis.dispatch({action: "view_welcome_page"});
|
||||
}
|
||||
});
|
||||
// Note we don't catch errors from this: we catch everything within
|
||||
|
@ -572,40 +571,16 @@ export default React.createClass({
|
|||
this._viewIndexedRoom(payload.roomIndex);
|
||||
break;
|
||||
case 'view_user_settings': {
|
||||
if (SettingsStore.isFeatureEnabled("feature_tabbed_settings")) {
|
||||
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
|
||||
Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}, 'mx_SettingsDialog');
|
||||
} else {
|
||||
this._setPage(PageTypes.UserSettings);
|
||||
this.notifyNewScreen('settings');
|
||||
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
|
||||
Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}, 'mx_SettingsDialog');
|
||||
|
||||
// View the home page if we need something to look at
|
||||
if (!this.state.currentGroupId && !this.state.currentRoomId) {
|
||||
this._setPage(PageTypes.HomePage);
|
||||
this.notifyNewScreen('home');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'view_old_user_settings':
|
||||
this._setPage(PageTypes.UserSettings);
|
||||
this.notifyNewScreen('settings');
|
||||
break;
|
||||
case 'close_settings':
|
||||
this.setState({
|
||||
leftDisabled: false,
|
||||
rightDisabled: false,
|
||||
middleDisabled: false,
|
||||
});
|
||||
if (this.state.page_type === PageTypes.UserSettings) {
|
||||
// We do this to get setPage and notifyNewScreen
|
||||
if (this.state.currentRoomId) {
|
||||
this._viewRoom({
|
||||
room_id: this.state.currentRoomId,
|
||||
});
|
||||
} else if (this.state.currentGroupId) {
|
||||
this._viewGroup({
|
||||
group_id: this.state.currentGroupId,
|
||||
});
|
||||
} else {
|
||||
this._viewHome();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'view_create_room':
|
||||
this._createRoom();
|
||||
break;
|
||||
|
@ -634,6 +609,9 @@ export default React.createClass({
|
|||
case 'view_group':
|
||||
this._viewGroup(payload);
|
||||
break;
|
||||
case 'view_welcome_page':
|
||||
this._viewWelcome();
|
||||
break;
|
||||
case 'view_home_page':
|
||||
this._viewHome();
|
||||
break;
|
||||
|
@ -909,6 +887,13 @@ export default React.createClass({
|
|||
this.notifyNewScreen('group/' + groupId);
|
||||
},
|
||||
|
||||
_viewWelcome() {
|
||||
this.setStateForNewView({
|
||||
view: VIEWS.WELCOME,
|
||||
});
|
||||
this.notifyNewScreen('welcome');
|
||||
},
|
||||
|
||||
_viewHome: function() {
|
||||
// The home page requires the "logged in" view, so we'll set that.
|
||||
this.setStateForNewView({
|
||||
|
@ -982,11 +967,11 @@ export default React.createClass({
|
|||
}
|
||||
dis.dispatch({
|
||||
action: 'require_registration',
|
||||
// If the set_mxid dialog is cancelled, view /home because if the browser
|
||||
// was pointing at /user/@someone:domain?action=chat, the URL needs to be
|
||||
// reset so that they can revisit /user/.. // (and trigger
|
||||
// If the set_mxid dialog is cancelled, view /welcome because if the
|
||||
// browser was pointing at /user/@someone:domain?action=chat, the URL
|
||||
// needs to be reset so that they can revisit /user/.. // (and trigger
|
||||
// `_chatCreateOrReuse` again)
|
||||
go_home_on_cancel: true,
|
||||
go_welcome_on_cancel: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -1063,7 +1048,6 @@ export default React.createClass({
|
|||
modal.close();
|
||||
if (this.state.currentRoomId === roomId) {
|
||||
dis.dispatch({action: 'view_next_room'});
|
||||
dis.dispatch({action: 'close_room_settings'});
|
||||
}
|
||||
}, (err) => {
|
||||
modal.close();
|
||||
|
@ -1209,7 +1193,11 @@ export default React.createClass({
|
|||
room_id: localStorage.getItem('mx_last_room_id'),
|
||||
});
|
||||
} else {
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
if (MatrixClientPeg.get().isGuest) {
|
||||
dis.dispatch({action: 'view_welcome_page'});
|
||||
} else {
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1495,6 +1483,10 @@ export default React.createClass({
|
|||
dis.dispatch({
|
||||
action: 'view_user_settings',
|
||||
});
|
||||
} else if (screen == 'welcome') {
|
||||
dis.dispatch({
|
||||
action: 'view_welcome_page',
|
||||
});
|
||||
} else if (screen == 'home') {
|
||||
dis.dispatch({
|
||||
action: 'view_home_page',
|
||||
|
@ -1676,11 +1668,6 @@ export default React.createClass({
|
|||
this.showScreen("forgot_password");
|
||||
},
|
||||
|
||||
onReturnToAppClick: function() {
|
||||
// treat it the same as if the user had completed the login
|
||||
this._onLoggedIn();
|
||||
},
|
||||
|
||||
// returns a promise which resolves to the new MatrixClient
|
||||
onRegistered: function(credentials) {
|
||||
// XXX: This should be in state or ideally store(s) because we risk not
|
||||
|
@ -1883,6 +1870,11 @@ export default React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
if (this.state.view === VIEWS.WELCOME) {
|
||||
const Welcome = sdk.getComponent('auth.Welcome');
|
||||
return <Welcome />;
|
||||
}
|
||||
|
||||
if (this.state.view === VIEWS.REGISTER) {
|
||||
const Registration = sdk.getComponent('structures.auth.Registration');
|
||||
return (
|
||||
|
@ -1891,7 +1883,6 @@ export default React.createClass({
|
|||
sessionId={this.state.register_session_id}
|
||||
idSid={this.state.register_id_sid}
|
||||
email={this.props.startingFragmentQueryParams.email}
|
||||
referrer={this.props.startingFragmentQueryParams.referrer}
|
||||
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
|
||||
defaultHsUrl={this.getDefaultHsUrl()}
|
||||
defaultIsUrl={this.getDefaultIsUrl()}
|
||||
|
@ -1899,11 +1890,8 @@ export default React.createClass({
|
|||
customHsUrl={this.getCurrentHsUrl()}
|
||||
customIsUrl={this.getCurrentIsUrl()}
|
||||
makeRegistrationUrl={this._makeRegistrationUrl}
|
||||
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
||||
onLoggedIn={this.onRegistered}
|
||||
onLoginClick={this.onLoginClick}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
/>
|
||||
);
|
||||
|
@ -1940,7 +1928,6 @@ export default React.createClass({
|
|||
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
||||
onForgotPasswordClick={this.onForgotPasswordClick}
|
||||
enableGuest={this.props.enableGuest}
|
||||
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -642,14 +642,13 @@ module.exports = React.createClass({
|
|||
|
||||
updateTimelineMinHeight: function() {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
const whoIsTyping = this.refs.whoIsTyping;
|
||||
const isTypingVisible = whoIsTyping && whoIsTyping.isVisible();
|
||||
|
||||
if (scrollPanel) {
|
||||
if (isTypingVisible) {
|
||||
const isAtBottom = scrollPanel.isAtBottom();
|
||||
const whoIsTyping = this.refs.whoIsTyping;
|
||||
const isTypingVisible = whoIsTyping && whoIsTyping.isVisible();
|
||||
if (isAtBottom && isTypingVisible) {
|
||||
scrollPanel.blockShrinking();
|
||||
} else {
|
||||
scrollPanel.clearBlockShrinking();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -24,10 +24,7 @@ const Modal = require('../../Modal');
|
|||
const sdk = require('../../index');
|
||||
const dis = require('../../dispatcher');
|
||||
|
||||
const linkify = require('linkifyjs');
|
||||
const linkifyString = require('linkifyjs/string');
|
||||
const linkifyMatrix = require('../../linkify-matrix');
|
||||
const sanitizeHtml = require('sanitize-html');
|
||||
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import { _t } from '../../languageHandler';
|
||||
|
@ -37,8 +34,6 @@ import {instanceForInstanceId, protocolNameForInstanceId} from '../../utils/Dire
|
|||
const MAX_NAME_LENGTH = 80;
|
||||
const MAX_TOPIC_LENGTH = 160;
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomDirectory',
|
||||
|
||||
|
@ -96,9 +91,9 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to get protocol list from Home Server', '', ErrorDialog, {
|
||||
title: _t('Failed to get protocol list from Home Server'),
|
||||
description: _t('The Home Server may be too old to support third party networks'),
|
||||
Modal.createTrackedDialog('Failed to get protocol list from homeserver', '', ErrorDialog, {
|
||||
title: _t('Failed to get protocol list from homeserver'),
|
||||
description: _t('The homeserver may be too old to support third party networks'),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -438,7 +433,7 @@ module.exports = React.createClass({
|
|||
if (topic.length > MAX_TOPIC_LENGTH) {
|
||||
topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
|
||||
}
|
||||
topic = linkifyString(sanitizeHtml(topic));
|
||||
topic = linkifyAndSanitizeHtml(topic);
|
||||
|
||||
rows.push(
|
||||
<tr key={ rooms[i].room_id }
|
||||
|
|
|
@ -290,7 +290,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
return <div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src={require("../../../res/img/warning.svg")} width="24" height="23" title={_t("Warning")} alt="" />
|
||||
<img src={require("../../../res/img/feather-icons/e2e/warning.svg")} width="24" height="24" title={_t("Warning")} alt="" />
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ title }
|
||||
|
@ -309,7 +309,7 @@ module.exports = React.createClass({
|
|||
if (this._shouldShowConnectionError()) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src={require("../../../res/img/warning.svg")} width="24" height="23" title="/!\ " alt="/!\ " />
|
||||
<img src={require("../../../res/img/feather-icons/e2e/warning.svg")} width="24" height="24" title="/!\ " alt="/!\ " />
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ _t('Connectivity to the server has been lost.') }
|
||||
|
|
|
@ -127,46 +127,6 @@ const RoomSubList = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_shouldShowNotifBadge: function(roomNotifState) {
|
||||
const showBadgeInStates = [RoomNotifs.ALL_MESSAGES, RoomNotifs.ALL_MESSAGES_LOUD];
|
||||
return showBadgeInStates.indexOf(roomNotifState) > -1;
|
||||
},
|
||||
|
||||
_shouldShowMentionBadge: function(roomNotifState) {
|
||||
return roomNotifState !== RoomNotifs.MUTE;
|
||||
},
|
||||
|
||||
/**
|
||||
* Total up all the notification counts from the rooms
|
||||
*
|
||||
* @returns {Array} The array takes the form [total, highlight] where highlight is a bool
|
||||
*/
|
||||
roomNotificationCount: function() {
|
||||
const self = this;
|
||||
|
||||
if (this.props.isInvite) {
|
||||
return [0, true];
|
||||
}
|
||||
|
||||
return this.props.list.reduce(function(result, room, index) {
|
||||
const roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const notificationCount = room.getUnreadNotificationCount();
|
||||
|
||||
const notifBadges = notificationCount > 0 && self._shouldShowNotifBadge(roomNotifState);
|
||||
const mentionBadges = highlight && self._shouldShowMentionBadge(roomNotifState);
|
||||
const badges = notifBadges || mentionBadges;
|
||||
|
||||
if (badges) {
|
||||
result[0] += notificationCount;
|
||||
if (highlight) {
|
||||
result[1] = true;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, [0, false]);
|
||||
},
|
||||
|
||||
_updateSubListCount: function() {
|
||||
// Force an update by setting the state to the current state
|
||||
// Doing it this way rather than using forceUpdate(), so that the shouldComponentUpdate()
|
||||
|
@ -197,22 +157,12 @@ const RoomSubList = React.createClass({
|
|||
// prevent the roomsublist collapsing
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// find first room which has notifications and switch to it
|
||||
for (const room of this.props.list) {
|
||||
const roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const notificationCount = room.getUnreadNotificationCount();
|
||||
|
||||
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge(roomNotifState);
|
||||
const mentionBadges = highlight && this._shouldShowMentionBadge(roomNotifState);
|
||||
|
||||
if (notifBadges || mentionBadges) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: room.roomId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const room = this.props.list.find(room => RoomNotifs.getRoomHasBadge(room));
|
||||
if (room) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: room.roomId,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -240,9 +190,11 @@ const RoomSubList = React.createClass({
|
|||
|
||||
_getHeaderJsx: function(isCollapsed) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const subListNotifications = this.roomNotificationCount();
|
||||
const subListNotifCount = subListNotifications[0];
|
||||
const subListNotifHighlight = subListNotifications[1];
|
||||
const subListNotifications = !this.props.isInvite ?
|
||||
RoomNotifs.aggregateNotificationCount(this.props.list) :
|
||||
{count: 0, highlight: true};
|
||||
const subListNotifCount = subListNotifications.count;
|
||||
const subListNotifHighlight = subListNotifications.highlight;
|
||||
|
||||
let badge;
|
||||
if (!this.props.collapsed) {
|
||||
|
|
|
@ -78,7 +78,7 @@ module.exports = React.createClass({
|
|||
// * invitedEmail (string) The email address that was invited to this room
|
||||
thirdPartyInvite: PropTypes.object,
|
||||
|
||||
// Any data about the room that would normally come from the Home Server
|
||||
// Any data about the room that would normally come from the homeserver
|
||||
// but has been passed out-of-band, eg. the room name and avatar URL
|
||||
// from an email invite (a workaround for the fact that we can't
|
||||
// get this information from the HS using an email invite).
|
||||
|
@ -119,8 +119,6 @@ module.exports = React.createClass({
|
|||
isInitialEventHighlighted: null,
|
||||
|
||||
forwardingEvent: null,
|
||||
editingRoomSettings: false,
|
||||
uploadingRoomSettings: false,
|
||||
numUnreadMessages: 0,
|
||||
draggingFile: false,
|
||||
searching: false,
|
||||
|
@ -168,6 +166,7 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
|
||||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus);
|
||||
MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
this._fetchMediaConfig();
|
||||
// Start listening for RoomViewStore updates
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
|
@ -228,11 +227,8 @@ module.exports = React.createClass({
|
|||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||
shouldPeek: RoomViewStore.shouldPeek(),
|
||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", RoomViewStore.getRoomId()),
|
||||
editingRoomSettings: RoomViewStore.isEditingSettings(),
|
||||
};
|
||||
|
||||
if (this.state.editingRoomSettings && !newState.editingRoomSettings) dis.dispatch({action: 'focus_composer'});
|
||||
|
||||
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
|
||||
console.log(
|
||||
'RVS update:',
|
||||
|
@ -457,6 +453,7 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus);
|
||||
MatrixClientPeg.get().removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
}
|
||||
|
||||
window.removeEventListener('beforeunload', this.onPageUnload);
|
||||
|
@ -589,6 +586,10 @@ module.exports = React.createClass({
|
|||
this._updatePreviewUrlVisibility(room);
|
||||
}
|
||||
|
||||
if (ev.getType() === "m.room.encryption") {
|
||||
this._updateE2EStatus(room);
|
||||
}
|
||||
|
||||
// ignore anything but real-time updates at the end of the room:
|
||||
// updates from pagination will happen when the paginate completes.
|
||||
if (toStartOfTimeline || !data || !data.liveEvent) return;
|
||||
|
@ -637,11 +638,11 @@ module.exports = React.createClass({
|
|||
// called when state.room is first initialised (either at initial load,
|
||||
// after a successful peek, or after we join the room).
|
||||
_onRoomLoaded: function(room) {
|
||||
this._warnAboutEncryption(room);
|
||||
this._calculatePeekRules(room);
|
||||
this._updatePreviewUrlVisibility(room);
|
||||
this._loadMembersIfJoined(room);
|
||||
this._calculateRecommendedVersion(room);
|
||||
this._updateE2EStatus(room);
|
||||
},
|
||||
|
||||
_calculateRecommendedVersion: async function(room) {
|
||||
|
@ -670,34 +671,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_warnAboutEncryption: function(room) {
|
||||
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) {
|
||||
return;
|
||||
}
|
||||
let userHasUsedEncryption = false;
|
||||
if (localStorage) {
|
||||
userHasUsedEncryption = localStorage.getItem('mx_user_has_used_encryption');
|
||||
}
|
||||
if (!userHasUsedEncryption) {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('E2E Warning', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
hasCancelButton: false,
|
||||
description: (
|
||||
<div>
|
||||
<p>{ _t("End-to-end encryption is in beta and may not be reliable") }.</p>
|
||||
<p>{ _t("You should not yet trust it to secure data") }.</p>
|
||||
<p>{ _t("Devices will not yet be able to decrypt history from before they joined the room") }.</p>
|
||||
<p>{ _t("Encrypted messages will not be visible on clients that do not yet implement encryption") }.</p>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
if (localStorage) {
|
||||
localStorage.setItem('mx_user_has_used_encryption', true);
|
||||
}
|
||||
},
|
||||
|
||||
_calculatePeekRules: function(room) {
|
||||
const guestAccessEvent = room.currentState.getStateEvents("m.room.guest_access", "");
|
||||
if (guestAccessEvent && guestAccessEvent.getContent().guest_access === "can_join") {
|
||||
|
@ -733,6 +706,23 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onDeviceVerificationChanged: function(userId, device) {
|
||||
const room = this.state.room;
|
||||
if (!room.currentState.getMember(userId)) {
|
||||
return;
|
||||
}
|
||||
this._updateE2EStatus(room);
|
||||
},
|
||||
|
||||
_updateE2EStatus: function(room) {
|
||||
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) {
|
||||
return;
|
||||
}
|
||||
room.hasUnverifiedDevices().then((hasUnverifiedDevices) => {
|
||||
this.setState({e2eStatus: hasUnverifiedDevices ? "warning" : "verified"});
|
||||
});
|
||||
},
|
||||
|
||||
updateTint: function() {
|
||||
const room = this.state.room;
|
||||
if (!room) return;
|
||||
|
@ -1122,7 +1112,7 @@ module.exports = React.createClass({
|
|||
// favour longer (more specific) terms first
|
||||
highlights = highlights.sort(function(a, b) {
|
||||
return b.length - a.length;
|
||||
});
|
||||
});
|
||||
|
||||
self.setState({
|
||||
searchHighlights: highlights,
|
||||
|
@ -1236,50 +1226,9 @@ module.exports = React.createClass({
|
|||
dis.dispatch({ action: 'open_room_settings' });
|
||||
},
|
||||
|
||||
onSettingsSaveClick: function() {
|
||||
if (!this.refs.room_settings) return;
|
||||
|
||||
this.setState({
|
||||
uploadingRoomSettings: true,
|
||||
});
|
||||
|
||||
const newName = this.refs.header.getEditedName();
|
||||
if (newName !== undefined) {
|
||||
this.refs.room_settings.setName(newName);
|
||||
}
|
||||
const newTopic = this.refs.header.getEditedTopic();
|
||||
if (newTopic !== undefined) {
|
||||
this.refs.room_settings.setTopic(newTopic);
|
||||
}
|
||||
|
||||
this.refs.room_settings.save().then((results) => {
|
||||
const fails = results.filter(function(result) { return result.state !== "fulfilled"; });
|
||||
console.log("Settings saved with %s errors", fails.length);
|
||||
if (fails.length) {
|
||||
fails.forEach(function(result) {
|
||||
console.error(result.reason);
|
||||
});
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to save room settings', '', ErrorDialog, {
|
||||
title: _t("Failed to save settings"),
|
||||
description: fails.map(function(result) { return result.reason; }).join("\n"),
|
||||
});
|
||||
// still editing room settings
|
||||
} else {
|
||||
dis.dispatch({ action: 'close_settings' });
|
||||
}
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
uploadingRoomSettings: false,
|
||||
});
|
||||
dis.dispatch({ action: 'close_settings' });
|
||||
}).done();
|
||||
},
|
||||
|
||||
onCancelClick: function() {
|
||||
console.log("updateTint from onCancelClick");
|
||||
this.updateTint();
|
||||
dis.dispatch({ action: 'close_settings' });
|
||||
if (this.state.forwardingEvent) {
|
||||
dis.dispatch({
|
||||
action: 'forward_event',
|
||||
|
@ -1437,7 +1386,7 @@ module.exports = React.createClass({
|
|||
(83 + // height of RoomHeader
|
||||
36 + // height of the status area
|
||||
72 + // minimum height of the message compmoser
|
||||
(this.state.editingRoomSettings ? (window.innerHeight * 0.3) : 120)); // amount of desired scrollback
|
||||
120); // amount of desired scrollback
|
||||
|
||||
// XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
|
||||
// but it's better than the video going missing entirely
|
||||
|
@ -1537,7 +1486,6 @@ module.exports = React.createClass({
|
|||
const RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
||||
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
||||
const ForwardMessage = sdk.getComponent("rooms.ForwardMessage");
|
||||
const RoomSettings = sdk.getComponent("rooms.RoomSettings");
|
||||
const AuxPanel = sdk.getComponent("rooms.AuxPanel");
|
||||
const SearchBar = sdk.getComponent("rooms.SearchBar");
|
||||
const PinnedEventsPanel = sdk.getComponent("rooms.PinnedEventsPanel");
|
||||
|
@ -1575,6 +1523,7 @@ module.exports = React.createClass({
|
|||
room={this.state.room}
|
||||
oobData={this.props.oobData}
|
||||
collapsedRhs={this.props.collapsedRhs}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
/>
|
||||
<div className="mx_RoomView_body">
|
||||
<div className="mx_RoomView_auxPanel">
|
||||
|
@ -1622,6 +1571,7 @@ module.exports = React.createClass({
|
|||
ref="header"
|
||||
room={this.state.room}
|
||||
collapsedRhs={this.props.collapsedRhs}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
/>
|
||||
<div className="mx_RoomView_body">
|
||||
<div className="mx_RoomView_auxPanel">
|
||||
|
@ -1685,7 +1635,6 @@ module.exports = React.createClass({
|
|||
);
|
||||
|
||||
const showRoomRecoveryReminder = (
|
||||
SettingsStore.isFeatureEnabled("feature_keybackup") &&
|
||||
SettingsStore.getValue("showRoomRecoveryReminder") &&
|
||||
MatrixClientPeg.get().isRoomEncrypted(this.state.room.roomId) &&
|
||||
!MatrixClientPeg.get().getKeyBackupEnabled()
|
||||
|
@ -1693,11 +1642,7 @@ module.exports = React.createClass({
|
|||
|
||||
let aux = null;
|
||||
let hideCancel = false;
|
||||
if (this.state.editingRoomSettings) {
|
||||
aux = <RoomSettings ref="room_settings" onSaveClick={this.onSettingsSaveClick} onCancelClick={this.onCancelClick} room={this.state.room} />;
|
||||
} else if (this.state.uploadingRoomSettings) {
|
||||
aux = <Loader />;
|
||||
} else if (this.state.forwardingEvent !== null) {
|
||||
if (this.state.forwardingEvent !== null) {
|
||||
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
|
||||
} else if (this.state.searching) {
|
||||
hideCancel = true; // has own cancel
|
||||
|
@ -1739,7 +1684,7 @@ module.exports = React.createClass({
|
|||
|
||||
const auxPanel = (
|
||||
<AuxPanel ref="auxPanel" room={this.state.room}
|
||||
fullHeight={this.state.editingRoomSettings}
|
||||
fullHeight={false}
|
||||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
conferenceHandler={this.props.ConferenceHandler}
|
||||
draggingFile={this.state.draggingFile}
|
||||
|
@ -1747,7 +1692,7 @@ module.exports = React.createClass({
|
|||
maxHeight={this.state.auxPanelMaxHeight}
|
||||
onResize={this.onChildResize}
|
||||
showApps={this.state.showApps}
|
||||
hideAppsDrawer={this.state.editingRoomSettings} >
|
||||
hideAppsDrawer={false} >
|
||||
{ aux }
|
||||
</AuxPanel>
|
||||
);
|
||||
|
@ -1767,6 +1712,7 @@ module.exports = React.createClass({
|
|||
disabled={this.props.disabled}
|
||||
showApps={this.state.showApps}
|
||||
uploadAllowed={this.isFileUploadAllowed}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
@ -1906,17 +1852,15 @@ module.exports = React.createClass({
|
|||
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
|
||||
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
||||
oobData={this.props.oobData}
|
||||
editing={this.state.editingRoomSettings}
|
||||
saving={this.state.uploadingRoomSettings}
|
||||
inRoom={myMembership === 'join'}
|
||||
collapsedRhs={this.props.collapsedRhs}
|
||||
onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick}
|
||||
onPinnedClick={this.onPinnedClick}
|
||||
onSaveClick={this.onSettingsSaveClick}
|
||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
/>
|
||||
<MainSplit panel={rightPanel} collapsedRhs={this.props.collapsedRhs}>
|
||||
<div className={fadableSectionClasses}>
|
||||
|
|
|
@ -169,6 +169,10 @@ module.exports = React.createClass({
|
|||
//
|
||||
// This will also re-check the fill state, in case the paginate was inadequate
|
||||
this.checkScroll();
|
||||
|
||||
if (!this.isAtBottom()) {
|
||||
this.clearBlockShrinking();
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
|
|
@ -71,7 +71,7 @@ module.exports = React.createClass({
|
|||
function() {
|
||||
this.props.onSearch(this.refs.search.value);
|
||||
},
|
||||
100,
|
||||
500,
|
||||
),
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
|
@ -95,8 +95,13 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
|
||||
// check for collapsed here and
|
||||
// not at parent so we keep
|
||||
// searchTerm in our state
|
||||
// when collapsing and expanding
|
||||
if (this.props.collapsed) {
|
||||
return null;
|
||||
}
|
||||
const clearButton = this.state.searchTerm.length > 0 ?
|
||||
(<AccessibleButton key="button"
|
||||
className="mx_SearchBox_closeButton"
|
||||
|
|
|
@ -74,7 +74,6 @@ export class TabbedView extends React.Component {
|
|||
|
||||
const idx = this.props.tabs.indexOf(tab);
|
||||
if (idx === this._getActiveTabIndex()) classes += "mx_TabbedView_tabLabel_active";
|
||||
if (tab.label === "Visit old settings") classes += "mx_TabbedView_tabLabel_TEMP_HACK";
|
||||
|
||||
let tabIcon = null;
|
||||
if (tab.icon) {
|
||||
|
@ -97,7 +96,9 @@ export class TabbedView extends React.Component {
|
|||
_renderTabPanel(tab) {
|
||||
return (
|
||||
<div className="mx_TabbedView_tabPanel" key={"mx_tabpanel_" + tab.label}>
|
||||
{tab.body}
|
||||
<div className='mx_TabbedView_tabPanelContent'>
|
||||
{tab.body}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import GroupActions from '../../actions/GroupActions';
|
|||
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import Modal from '../../Modal';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
|
@ -48,8 +47,6 @@ const TagPanel = React.createClass({
|
|||
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
|
||||
this.context.matrixClient.on("sync", this._onClientSync);
|
||||
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
|
||||
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
|
@ -70,9 +67,6 @@ const TagPanel = React.createClass({
|
|||
if (this._filterStoreToken) {
|
||||
this._filterStoreToken.remove();
|
||||
}
|
||||
if (this._dispatcherRef) {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
}
|
||||
},
|
||||
|
||||
_onGroupMyMembership() {
|
||||
|
@ -106,21 +100,11 @@ const TagPanel = React.createClass({
|
|||
dis.dispatch({action: 'deselect_tags'});
|
||||
},
|
||||
|
||||
_onAction(payload) {
|
||||
if (payload.action === "show_redesign_feedback_dialog") {
|
||||
const RedesignFeedbackDialog =
|
||||
sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
|
||||
Modal.createDialog(RedesignFeedbackDialog);
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
const GroupsButton = sdk.getComponent('elements.GroupsButton');
|
||||
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
const ActionButton = sdk.getComponent("elements.ActionButton");
|
||||
|
||||
const tags = this.state.orderedTags.map((tag, index) => {
|
||||
return <DNDTagTile
|
||||
|
@ -174,13 +158,6 @@ const TagPanel = React.createClass({
|
|||
) }
|
||||
</Droppable>
|
||||
</GeminiScrollbarWrapper>
|
||||
<div className="mx_TagPanel_divider" />
|
||||
<div className="mx_TagPanel_groupsButton">
|
||||
<GroupsButton />
|
||||
<ActionButton
|
||||
className="mx_TagPanel_report" action="show_redesign_feedback_dialog"
|
||||
label={_t("Report bugs & give feedback")} tooltip={true} />
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
|
|
58
src/components/structures/TagPanelButtons.js
Normal file
58
src/components/structures/TagPanelButtons.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd.
|
||||
|
||||
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 sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import Modal from '../../Modal';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
const TagPanelButtons = React.createClass({
|
||||
displayName: 'TagPanelButtons',
|
||||
|
||||
|
||||
componentWillMount: function() {
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._dispatcherRef) {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
this._dispatcherRef = null;
|
||||
}
|
||||
},
|
||||
|
||||
_onAction(payload) {
|
||||
if (payload.action === "show_redesign_feedback_dialog") {
|
||||
const RedesignFeedbackDialog =
|
||||
sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
|
||||
Modal.createDialog(RedesignFeedbackDialog);
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
const GroupsButton = sdk.getComponent('elements.GroupsButton');
|
||||
const ActionButton = sdk.getComponent("elements.ActionButton");
|
||||
|
||||
return (<div className="mx_TagPanelButtons">
|
||||
<GroupsButton />
|
||||
<ActionButton
|
||||
className="mx_TagPanelButtons_report" action="show_redesign_feedback_dialog"
|
||||
label={_t("Report bugs & give feedback")} tooltip={true} />
|
||||
</div>);
|
||||
},
|
||||
});
|
||||
export default TagPanelButtons;
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2017, 2018, 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -25,6 +25,18 @@ import SdkConfig from "../../../SdkConfig";
|
|||
|
||||
import PasswordReset from "../../../PasswordReset";
|
||||
|
||||
// Phases
|
||||
// Show controls to configure server details
|
||||
const PHASE_SERVER_DETAILS = 0;
|
||||
// Show the forgot password inputs
|
||||
const PHASE_FORGOT = 1;
|
||||
// Email is in the process of being sent
|
||||
const PHASE_SENDING_EMAIL = 2;
|
||||
// Email has been sent
|
||||
const PHASE_EMAIL_SENT = 3;
|
||||
// User has clicked the link in email and completed reset
|
||||
const PHASE_DONE = 4;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ForgotPassword',
|
||||
|
||||
|
@ -47,28 +59,29 @@ module.exports = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||
progress: null,
|
||||
password: null,
|
||||
password2: null,
|
||||
enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||
enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||
phase: PHASE_FORGOT,
|
||||
email: "",
|
||||
password: "",
|
||||
password2: "",
|
||||
errorText: null,
|
||||
};
|
||||
},
|
||||
|
||||
submitPasswordReset: function(hsUrl, identityUrl, email, password) {
|
||||
this.setState({
|
||||
progress: "sending_email",
|
||||
phase: PHASE_SENDING_EMAIL,
|
||||
});
|
||||
this.reset = new PasswordReset(hsUrl, identityUrl);
|
||||
this.reset.resetPassword(email, password).done(() => {
|
||||
this.setState({
|
||||
progress: "sent_email",
|
||||
phase: PHASE_EMAIL_SENT,
|
||||
});
|
||||
}, (err) => {
|
||||
this.showErrorDialog(_t('Failed to send email') + ": " + err.message);
|
||||
this.setState({
|
||||
progress: null,
|
||||
phase: PHASE_FORGOT,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -80,7 +93,7 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
this.reset.checkEmailLinkClicked().done((res) => {
|
||||
this.setState({ progress: "complete" });
|
||||
this.setState({ phase: PHASE_DONE });
|
||||
}, (err) => {
|
||||
this.showErrorDialog(err.message);
|
||||
});
|
||||
|
@ -126,7 +139,7 @@ module.exports = React.createClass({
|
|||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.submitPasswordReset(
|
||||
this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
|
||||
this.state.enteredHsUrl, this.state.enteredIsUrl,
|
||||
this.state.email, this.state.password,
|
||||
);
|
||||
}
|
||||
|
@ -153,14 +166,29 @@ module.exports = React.createClass({
|
|||
onServerConfigChange: function(config) {
|
||||
const newState = {};
|
||||
if (config.hsUrl !== undefined) {
|
||||
newState.enteredHomeserverUrl = config.hsUrl;
|
||||
newState.enteredHsUrl = config.hsUrl;
|
||||
}
|
||||
if (config.isUrl !== undefined) {
|
||||
newState.enteredIdentityServerUrl = config.isUrl;
|
||||
newState.enteredIsUrl = config.isUrl;
|
||||
}
|
||||
this.setState(newState);
|
||||
},
|
||||
|
||||
onServerDetailsNextPhaseClick(ev) {
|
||||
ev.stopPropagation();
|
||||
this.setState({
|
||||
phase: PHASE_FORGOT,
|
||||
});
|
||||
},
|
||||
|
||||
onEditServerDetailsClick(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.setState({
|
||||
phase: PHASE_SERVER_DETAILS,
|
||||
});
|
||||
},
|
||||
|
||||
onLoginClick: function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
@ -175,95 +203,152 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
renderServerDetails() {
|
||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div>
|
||||
<ServerConfig
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
customHsUrl={this.state.enteredHsUrl}
|
||||
customIsUrl={this.state.enteredIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={0} />
|
||||
<AccessibleButton className="mx_Login_submit"
|
||||
onClick={this.onServerDetailsNextPhaseClick}
|
||||
>
|
||||
{_t("Next")}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderForgot() {
|
||||
let errorText = null;
|
||||
const err = this.state.errorText || this.props.defaultServerDiscoveryError;
|
||||
if (err) {
|
||||
errorText = <div className="mx_Login_error">{ err }</div>;
|
||||
}
|
||||
|
||||
let yourMatrixAccountText = _t('Your account');
|
||||
try {
|
||||
const parsedHsUrl = new URL(this.state.enteredHsUrl);
|
||||
yourMatrixAccountText = _t('Your account on %(serverName)s', {
|
||||
serverName: parsedHsUrl.hostname,
|
||||
});
|
||||
} catch (e) {
|
||||
errorText = <div className="mx_Login_error">{_t(
|
||||
"The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please " +
|
||||
"enter a valid URL including the protocol prefix.",
|
||||
{
|
||||
hsUrl: this.state.enteredHsUrl,
|
||||
})}</div>;
|
||||
}
|
||||
|
||||
// If custom URLs are allowed, wire up the server details edit link.
|
||||
let editLink = null;
|
||||
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||
editLink = <a className="mx_AuthBody_editServerDetails"
|
||||
href="#" onClick={this.onEditServerDetailsClick}
|
||||
>
|
||||
{_t('Change')}
|
||||
</a>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
<h3>
|
||||
{yourMatrixAccountText}
|
||||
{editLink}
|
||||
</h3>
|
||||
{errorText}
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
<input className="mx_Login_field" type="text"
|
||||
name="reset_email" // define a name so browser's password autofill gets less confused
|
||||
value={this.state.email}
|
||||
onChange={this.onInputChanged.bind(this, "email")}
|
||||
placeholder={_t('Email')} autoFocus />
|
||||
</div>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
<input className="mx_Login_field" type="password"
|
||||
name="reset_password"
|
||||
value={this.state.password}
|
||||
onChange={this.onInputChanged.bind(this, "password")}
|
||||
placeholder={_t('Password')} />
|
||||
<input className="mx_Login_field" type="password"
|
||||
name="reset_password_confirm"
|
||||
value={this.state.password2}
|
||||
onChange={this.onInputChanged.bind(this, "password2")}
|
||||
placeholder={_t('Confirm')} />
|
||||
</div>
|
||||
<span>{_t(
|
||||
'A verification email will be sent to your inbox to confirm ' +
|
||||
'setting your new password.',
|
||||
)}</span>
|
||||
<input className="mx_Login_submit" type="submit" value={_t('Send Reset Email')} />
|
||||
</form>
|
||||
<a className="mx_AuthBody_changeFlow" onClick={this.onLoginClick} href="#">
|
||||
{_t('Sign in instead')}
|
||||
</a>
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderSendingEmail() {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <Spinner />;
|
||||
},
|
||||
|
||||
renderEmailSent() {
|
||||
return <div>
|
||||
{_t("An email has been sent to %(emailAddress)s. Once you've followed the " +
|
||||
"link it contains, click below.", { emailAddress: this.state.email })}
|
||||
<br />
|
||||
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
||||
value={_t('I have verified my email address')} />
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderDone() {
|
||||
return <div>
|
||||
<p>{_t("Your password has been reset.")}</p>
|
||||
<p>{_t(
|
||||
"You have been logged out of all devices and will no longer receive " +
|
||||
"push notifications. To re-enable notifications, sign in again on each " +
|
||||
"device.",
|
||||
)}</p>
|
||||
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
||||
value={_t('Return to login screen')} />
|
||||
</div>;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||
const AuthHeader = sdk.getComponent("auth.AuthHeader");
|
||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
let resetPasswordJsx;
|
||||
|
||||
if (this.state.progress === "sending_email") {
|
||||
resetPasswordJsx = <Spinner />;
|
||||
} else if (this.state.progress === "sent_email") {
|
||||
resetPasswordJsx = (
|
||||
<div>
|
||||
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, " +
|
||||
"click below.", { emailAddress: this.state.email }) }
|
||||
<br />
|
||||
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
||||
value={_t('I have verified my email address')} />
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.progress === "complete") {
|
||||
resetPasswordJsx = (
|
||||
<div>
|
||||
<p>{ _t('Your password has been reset') }.</p>
|
||||
<p>{ _t('You have been logged out of all devices and will no longer receive push notifications. ' +
|
||||
'To re-enable notifications, sign in again on each device') }.</p>
|
||||
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
||||
value={_t('Return to login screen')} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
let serverConfigSection;
|
||||
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||
serverConfigSection = (
|
||||
<ServerConfig ref="serverConfig"
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={0} />
|
||||
);
|
||||
}
|
||||
|
||||
let errorText = null;
|
||||
const err = this.state.errorText || this.props.defaultServerDiscoveryError;
|
||||
if (err) {
|
||||
errorText = <div className="mx_Login_error">{ err }</div>;
|
||||
}
|
||||
|
||||
resetPasswordJsx = (
|
||||
<div>
|
||||
<p>
|
||||
{ _t('To reset your password, enter the email address linked to your account') }:
|
||||
</p>
|
||||
<div>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
<input className="mx_Login_field" ref="user" type="text"
|
||||
name="reset_email" // define a name so browser's password autofill gets less confused
|
||||
value={this.state.email}
|
||||
onChange={this.onInputChanged.bind(this, "email")}
|
||||
placeholder={_t('Email address')} autoFocus />
|
||||
<br />
|
||||
<input className="mx_Login_field" ref="pass" type="password"
|
||||
name="reset_password"
|
||||
value={this.state.password}
|
||||
onChange={this.onInputChanged.bind(this, "password")}
|
||||
placeholder={_t('New password')} />
|
||||
<br />
|
||||
<input className="mx_Login_field" ref="pass" type="password"
|
||||
name="reset_password_confirm"
|
||||
value={this.state.password2}
|
||||
onChange={this.onInputChanged.bind(this, "password2")}
|
||||
placeholder={_t('Confirm your new password')} />
|
||||
<br />
|
||||
<input className="mx_Login_submit" type="submit" value={_t('Send Reset Email')} />
|
||||
</form>
|
||||
{ serverConfigSection }
|
||||
{ errorText }
|
||||
<a className="mx_AuthBody_changeFlow" onClick={this.onLoginClick} href="#">
|
||||
{ _t('Sign in instead') }
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
switch (this.state.phase) {
|
||||
case PHASE_SERVER_DETAILS:
|
||||
resetPasswordJsx = this.renderServerDetails();
|
||||
break;
|
||||
case PHASE_FORGOT:
|
||||
resetPasswordJsx = this.renderForgot();
|
||||
break;
|
||||
case PHASE_SENDING_EMAIL:
|
||||
resetPasswordJsx = this.renderSendingEmail();
|
||||
break;
|
||||
case PHASE_EMAIL_SENT:
|
||||
resetPasswordJsx = this.renderEmailSent();
|
||||
break;
|
||||
case PHASE_DONE:
|
||||
resetPasswordJsx = this.renderDone();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
<AuthHeader />
|
||||
|
|
|
@ -26,7 +26,6 @@ import Login from '../../../Login';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
import { AutoDiscovery } from "matrix-js-sdk";
|
||||
import * as ServerType from '../../views/auth/ServerTypeSelector';
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
|
@ -37,8 +36,8 @@ const PHASE_SERVER_DETAILS = 0;
|
|||
// Show the appropriate login flow(s) for the server
|
||||
const PHASE_LOGIN = 1;
|
||||
|
||||
// Disable phases for now, pending UX discussion on WK discovery
|
||||
const PHASES_ENABLED = false;
|
||||
// Enable phases for login
|
||||
const PHASES_ENABLED = true;
|
||||
|
||||
// These are used in several places, and come from the js-sdk's autodiscovery
|
||||
// stuff. We define them here so that they'll be picked up by i18n.
|
||||
|
@ -63,7 +62,7 @@ module.exports = React.createClass({
|
|||
defaultIsUrl: PropTypes.string,
|
||||
// Secondary HS which we try to log into if the user is using
|
||||
// the default HS but login fails. Useful for migrating to a
|
||||
// different home server without confusing users.
|
||||
// different homeserver without confusing users.
|
||||
fallbackHsUrl: PropTypes.string,
|
||||
|
||||
// An error passed along from higher up explaining that something
|
||||
|
@ -77,7 +76,6 @@ module.exports = React.createClass({
|
|||
|
||||
// login shouldn't care how password recovery is done.
|
||||
onForgotPasswordClick: PropTypes.func,
|
||||
onCancelClick: PropTypes.func,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
|
@ -87,9 +85,8 @@ module.exports = React.createClass({
|
|||
errorText: null,
|
||||
loginIncorrect: false,
|
||||
|
||||
serverType: null,
|
||||
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||
enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||
enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||
|
||||
// used for preserving form values when changing homeserver
|
||||
username: "",
|
||||
|
@ -97,13 +94,11 @@ module.exports = React.createClass({
|
|||
phoneNumber: "",
|
||||
|
||||
// Phase of the overall login dialog.
|
||||
phase: PHASE_SERVER_DETAILS,
|
||||
phase: PHASE_LOGIN,
|
||||
// The current login flow, such as password, SSO, etc.
|
||||
currentFlow: "m.login.password",
|
||||
|
||||
// .well-known discovery
|
||||
discoveredHsUrl: "",
|
||||
discoveredIsUrl: "",
|
||||
discoveryError: "",
|
||||
findingHomeserver: false,
|
||||
};
|
||||
|
@ -160,7 +155,7 @@ module.exports = React.createClass({
|
|||
// Some error strings only apply for logging in
|
||||
const usingEmail = username.indexOf("@") > 0;
|
||||
if (error.httpStatus === 400 && usingEmail) {
|
||||
errorText = _t('This Home Server does not support login using email address.');
|
||||
errorText = _t('This homeserver does not support login using email address.');
|
||||
} else if (error.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
const errorTop = messageForResourceLimitError(
|
||||
error.data.limit_type,
|
||||
|
@ -241,7 +236,7 @@ module.exports = React.createClass({
|
|||
}, function(error) {
|
||||
let errorText;
|
||||
if (error.httpStatus === 403) {
|
||||
errorText = _t("Guest access is disabled on this Home Server.");
|
||||
errorText = _t("Guest access is disabled on this homeserver.");
|
||||
} else {
|
||||
errorText = self._errorTextFromError(error);
|
||||
}
|
||||
|
@ -308,10 +303,10 @@ module.exports = React.createClass({
|
|||
errorText: null, // reset err messages
|
||||
};
|
||||
if (config.hsUrl !== undefined) {
|
||||
newState.enteredHomeserverUrl = config.hsUrl;
|
||||
newState.enteredHsUrl = config.hsUrl;
|
||||
}
|
||||
if (config.isUrl !== undefined) {
|
||||
newState.enteredIdentityServerUrl = config.isUrl;
|
||||
newState.enteredIsUrl = config.isUrl;
|
||||
}
|
||||
|
||||
this.props.onServerConfigChange(config);
|
||||
|
@ -320,39 +315,6 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onServerTypeChange(type) {
|
||||
this.setState({
|
||||
serverType: type,
|
||||
});
|
||||
|
||||
// When changing server types, set the HS / IS URLs to reasonable defaults for the
|
||||
// the new type.
|
||||
switch (type) {
|
||||
case ServerType.FREE: {
|
||||
const { hsUrl, isUrl } = ServerType.TYPES.FREE;
|
||||
this.onServerConfigChange({
|
||||
hsUrl,
|
||||
isUrl,
|
||||
});
|
||||
// Move directly to the login phase since the server details are fixed.
|
||||
this.setState({
|
||||
phase: PHASE_LOGIN,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ServerType.PREMIUM:
|
||||
case ServerType.ADVANCED:
|
||||
this.onServerConfigChange({
|
||||
hsUrl: this.props.defaultHsUrl,
|
||||
isUrl: this.props.defaultIsUrl,
|
||||
});
|
||||
this.setState({
|
||||
phase: PHASE_SERVER_DETAILS,
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onRegisterClick: function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
@ -377,43 +339,42 @@ module.exports = React.createClass({
|
|||
_tryWellKnownDiscovery: async function(serverName) {
|
||||
if (!serverName.trim()) {
|
||||
// Nothing to discover
|
||||
this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: "", findingHomeserver: false});
|
||||
this.setState({
|
||||
discoveryError: "",
|
||||
findingHomeserver: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({findingHomeserver: true});
|
||||
try {
|
||||
const discovery = await AutoDiscovery.findClientConfig(serverName);
|
||||
|
||||
const state = discovery["m.homeserver"].state;
|
||||
if (state !== AutoDiscovery.SUCCESS && state !== AutoDiscovery.PROMPT) {
|
||||
this.setState({
|
||||
discoveredHsUrl: "",
|
||||
discoveredIsUrl: "",
|
||||
discoveryError: discovery["m.homeserver"].error,
|
||||
findingHomeserver: false,
|
||||
});
|
||||
} else if (state === AutoDiscovery.PROMPT) {
|
||||
this.setState({
|
||||
discoveredHsUrl: "",
|
||||
discoveredIsUrl: "",
|
||||
discoveryError: "",
|
||||
findingHomeserver: false,
|
||||
});
|
||||
} else if (state === AutoDiscovery.SUCCESS) {
|
||||
this.setState({
|
||||
discoveredHsUrl: discovery["m.homeserver"].base_url,
|
||||
discoveredIsUrl:
|
||||
discovery["m.identity_server"].state === AutoDiscovery.SUCCESS
|
||||
? discovery["m.identity_server"].base_url
|
||||
: "",
|
||||
discoveryError: "",
|
||||
findingHomeserver: false,
|
||||
});
|
||||
this.onServerConfigChange({
|
||||
hsUrl: discovery["m.homeserver"].base_url,
|
||||
isUrl: discovery["m.identity_server"].state === AutoDiscovery.SUCCESS
|
||||
? discovery["m.identity_server"].base_url
|
||||
: "",
|
||||
});
|
||||
} else {
|
||||
console.warn("Unknown state for m.homeserver in discovery response: ", discovery);
|
||||
this.setState({
|
||||
discoveredHsUrl: "",
|
||||
discoveredIsUrl: "",
|
||||
discoveryError: _t("Unknown failure discovering homeserver"),
|
||||
findingHomeserver: false,
|
||||
});
|
||||
|
@ -429,8 +390,8 @@ module.exports = React.createClass({
|
|||
|
||||
_initLoginLogic: function(hsUrl, isUrl) {
|
||||
const self = this;
|
||||
hsUrl = hsUrl || this.state.enteredHomeserverUrl;
|
||||
isUrl = isUrl || this.state.enteredIdentityServerUrl;
|
||||
hsUrl = hsUrl || this.state.enteredHsUrl;
|
||||
isUrl = isUrl || this.state.enteredIsUrl;
|
||||
|
||||
const fallbackHsUrl = hsUrl === this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
|
||||
|
||||
|
@ -440,8 +401,8 @@ module.exports = React.createClass({
|
|||
this._loginLogic = loginLogic;
|
||||
|
||||
this.setState({
|
||||
enteredHomeserverUrl: hsUrl,
|
||||
enteredIdentityServerUrl: isUrl,
|
||||
enteredHsUrl: hsUrl,
|
||||
enteredIsUrl: isUrl,
|
||||
busy: true,
|
||||
loginIncorrect: false,
|
||||
});
|
||||
|
@ -507,8 +468,8 @@ module.exports = React.createClass({
|
|||
|
||||
if (err.cors === 'rejected') {
|
||||
if (window.location.protocol === 'https:' &&
|
||||
(this.state.enteredHomeserverUrl.startsWith("http:") ||
|
||||
!this.state.enteredHomeserverUrl.startsWith("http"))
|
||||
(this.state.enteredHsUrl.startsWith("http:") ||
|
||||
!this.state.enteredHsUrl.startsWith("http"))
|
||||
) {
|
||||
errorText = <span>
|
||||
{ _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
|
||||
|
@ -532,7 +493,7 @@ module.exports = React.createClass({
|
|||
{
|
||||
'a': (sub) => {
|
||||
return <a target="_blank" rel="noopener"
|
||||
href={this.state.enteredHomeserverUrl}
|
||||
href={this.state.enteredHsUrl}
|
||||
>{ sub }</a>;
|
||||
},
|
||||
},
|
||||
|
@ -544,52 +505,26 @@ module.exports = React.createClass({
|
|||
return errorText;
|
||||
},
|
||||
|
||||
renderServerComponentForStep() {
|
||||
const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector");
|
||||
renderServerComponent() {
|
||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||
const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
// TODO: May need to adjust the behavior of this config option
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we're on a different phase, we only show the server type selector,
|
||||
// which is always shown if we allow custom URLs at all.
|
||||
if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS) {
|
||||
return <div>
|
||||
<ServerTypeSelector
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
onChange={this.onServerTypeChange}
|
||||
/>
|
||||
</div>;
|
||||
return null;
|
||||
}
|
||||
|
||||
let serverDetails = null;
|
||||
switch (this.state.serverType) {
|
||||
case ServerType.FREE:
|
||||
break;
|
||||
case ServerType.PREMIUM:
|
||||
serverDetails = <ModularServerConfig
|
||||
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={250}
|
||||
/>;
|
||||
break;
|
||||
case ServerType.ADVANCED:
|
||||
serverDetails = <ServerConfig
|
||||
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
|
||||
customIsUrl={this.state.discoveredIsUrl || this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={250}
|
||||
/>;
|
||||
break;
|
||||
}
|
||||
const serverDetails = <ServerConfig
|
||||
customHsUrl={this.state.enteredHsUrl}
|
||||
customIsUrl={this.state.enteredIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={250}
|
||||
/>;
|
||||
|
||||
let nextButton = null;
|
||||
if (PHASES_ENABLED) {
|
||||
|
@ -601,10 +536,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
return <div>
|
||||
<ServerTypeSelector
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
onChange={this.onServerTypeChange}
|
||||
/>
|
||||
{serverDetails}
|
||||
{nextButton}
|
||||
</div>;
|
||||
|
@ -633,13 +564,8 @@ module.exports = React.createClass({
|
|||
_renderPasswordStep: function() {
|
||||
const PasswordLogin = sdk.getComponent('auth.PasswordLogin');
|
||||
let onEditServerDetailsClick = null;
|
||||
// If custom URLs are allowed and we haven't selected the Free server type, wire
|
||||
// up the server details edit link.
|
||||
if (
|
||||
PHASES_ENABLED &&
|
||||
!SdkConfig.get()['disable_custom_urls'] &&
|
||||
this.state.serverType !== ServerType.FREE
|
||||
) {
|
||||
// If custom URLs are allowed, wire up the server details edit link.
|
||||
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
|
||||
onEditServerDetailsClick = this.onEditServerDetailsClick;
|
||||
}
|
||||
return (
|
||||
|
@ -657,7 +583,7 @@ module.exports = React.createClass({
|
|||
onPhoneNumberBlur={this.onPhoneNumberBlur}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||
loginIncorrect={this.state.loginIncorrect}
|
||||
hsUrl={this.state.enteredHomeserverUrl}
|
||||
hsUrl={this.state.enteredHsUrl}
|
||||
disableSubmit={this.state.findingHomeserver}
|
||||
/>
|
||||
);
|
||||
|
@ -708,11 +634,11 @@ module.exports = React.createClass({
|
|||
<AuthHeader />
|
||||
<AuthBody>
|
||||
<h2>
|
||||
{_t('Sign in to your account')}
|
||||
{_t('Sign in')}
|
||||
{loader}
|
||||
</h2>
|
||||
{ errorTextSection }
|
||||
{ this.renderServerComponentForStep() }
|
||||
{ this.renderServerComponent() }
|
||||
{ this.renderLoginComponentForStep() }
|
||||
<a className="mx_AuthBody_changeFlow" onClick={this.onRegisterClick} href="#">
|
||||
{ _t('Create account') }
|
||||
|
|
|
@ -54,21 +54,17 @@ module.exports = React.createClass({
|
|||
defaultIsUrl: PropTypes.string,
|
||||
brand: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
referrer: PropTypes.string,
|
||||
|
||||
// An error passed along from higher up explaining that something
|
||||
// went wrong when finding the defaultHsUrl.
|
||||
defaultServerDiscoveryError: PropTypes.string,
|
||||
|
||||
defaultDeviceDisplayName: PropTypes.string,
|
||||
|
||||
// registration shouldn't know or care how login is done.
|
||||
onLoginClick: PropTypes.func.isRequired,
|
||||
onCancelClick: PropTypes.func,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
const customURLsAllowed = !SdkConfig.get()['disable_custom_urls'];
|
||||
|
||||
return {
|
||||
busy: false,
|
||||
errorText: null,
|
||||
|
@ -90,6 +86,8 @@ module.exports = React.createClass({
|
|||
serverType: null,
|
||||
hsUrl: this.props.customHsUrl,
|
||||
isUrl: this.props.customIsUrl,
|
||||
// Phase of the overall registration dialog.
|
||||
phase: customURLsAllowed ? PHASE_SERVER_DETAILS : PHASE_REGISTRATION,
|
||||
flows: null,
|
||||
};
|
||||
},
|
||||
|
@ -164,9 +162,13 @@ module.exports = React.createClass({
|
|||
this.setState({
|
||||
flows: e.data.flows,
|
||||
});
|
||||
} else if (e.httpStatus === 403 && e.errcode === "M_UNKNOWN") {
|
||||
this.setState({
|
||||
errorText: _t("Registration has been disabled on this homeserver."),
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
errorText: _t("Unable to query for supported registration methods"),
|
||||
errorText: _t("Unable to query for supported registration methods."),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -363,7 +365,6 @@ module.exports = React.createClass({
|
|||
const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
// TODO: May need to adjust the behavior of this config option
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -141,7 +141,7 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div ref="recaptchaContainer">
|
||||
{ _t("This Home Server would like to make sure you are not a robot") }
|
||||
{ _t("This homeserver would like to make sure you are not a robot.") }
|
||||
<br />
|
||||
<div id={DIV_ID}></div>
|
||||
{ error }
|
||||
|
|
|
@ -334,7 +334,7 @@ export const TermsAuthEntry = React.createClass({
|
|||
let submitButton;
|
||||
if (this.props.showContinue !== false) {
|
||||
// XXX: button classes
|
||||
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_UserSettings_button"
|
||||
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton"
|
||||
onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
|
||||
}
|
||||
|
||||
|
@ -525,7 +525,7 @@ export const MsisdnAuthEntry = React.createClass({
|
|||
const enableSubmit = Boolean(this.state.token);
|
||||
const submitClasses = classnames({
|
||||
mx_InteractiveAuthEntryComponents_msisdnSubmit: true,
|
||||
mx_UserSettings_button: true, // XXX button classes
|
||||
mx_GeneralButton: true,
|
||||
});
|
||||
let errorSection;
|
||||
if (this.state.errorText) {
|
||||
|
|
|
@ -249,10 +249,10 @@ class PasswordLogin extends React.Component {
|
|||
</span>;
|
||||
}
|
||||
|
||||
let yourMatrixAccountText = _t('Your account');
|
||||
let signInToText = _t('Sign in');
|
||||
try {
|
||||
const parsedHsUrl = new URL(this.props.hsUrl);
|
||||
yourMatrixAccountText = _t('Your %(serverName)s account', {
|
||||
signInToText = _t('Sign in to %(serverName)s', {
|
||||
serverName: parsedHsUrl.hostname,
|
||||
});
|
||||
} catch (e) {
|
||||
|
@ -264,7 +264,7 @@ class PasswordLogin extends React.Component {
|
|||
editLink = <a className="mx_AuthBody_editServerDetails"
|
||||
href="#" onClick={this.props.onEditServerDetailsClick}
|
||||
>
|
||||
{_t('Edit')}
|
||||
{_t('Change')}
|
||||
</a>;
|
||||
}
|
||||
|
||||
|
@ -297,7 +297,7 @@ class PasswordLogin extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
<h3>
|
||||
{yourMatrixAccountText}
|
||||
{signInToText}
|
||||
{editLink}
|
||||
</h3>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
|
|
|
@ -270,11 +270,27 @@ module.exports = React.createClass({
|
|||
this.validateField(FIELD_USERNAME, ev.type);
|
||||
},
|
||||
|
||||
/**
|
||||
* A step is required if all flows include that step.
|
||||
*
|
||||
* @param {string} step A stage name to check
|
||||
* @returns {boolean} Whether it is required
|
||||
*/
|
||||
_authStepIsRequired(step) {
|
||||
// A step is required if no flow exists which does not include that step
|
||||
// (Notwithstanding setups like either email or msisdn being required)
|
||||
return !this.props.flows.some((flow) => {
|
||||
return !flow.stages.includes(step);
|
||||
return this.props.flows.every((flow) => {
|
||||
return flow.stages.includes(step);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* A step is used if any flows include that step.
|
||||
*
|
||||
* @param {string} step A stage name to check
|
||||
* @returns {boolean} Whether it is used
|
||||
*/
|
||||
_authStepIsUsed(step) {
|
||||
return this.props.flows.some((flow) => {
|
||||
return flow.stages.includes(step);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -298,24 +314,28 @@ module.exports = React.createClass({
|
|||
</a>;
|
||||
}
|
||||
|
||||
const emailPlaceholder = this._authStepIsRequired('m.login.email.identity') ?
|
||||
_t("Email") :
|
||||
_t("Email (optional)");
|
||||
let emailSection;
|
||||
if (this._authStepIsUsed('m.login.email.identity')) {
|
||||
const emailPlaceholder = this._authStepIsRequired('m.login.email.identity') ?
|
||||
_t("Email") :
|
||||
_t("Email (optional)");
|
||||
|
||||
const emailSection = (
|
||||
<div>
|
||||
<input type="text" ref="email"
|
||||
autoFocus={true} placeholder={emailPlaceholder}
|
||||
defaultValue={this.props.defaultEmail}
|
||||
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
||||
onBlur={this.onEmailBlur}
|
||||
value={this.state.email} />
|
||||
</div>
|
||||
);
|
||||
emailSection = (
|
||||
<div>
|
||||
<input type="text" ref="email"
|
||||
placeholder={emailPlaceholder}
|
||||
defaultValue={this.props.defaultEmail}
|
||||
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
||||
onBlur={this.onEmailBlur}
|
||||
value={this.state.email} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const threePidLogin = !SdkConfig.get().disable_3pid_login;
|
||||
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
|
||||
let phoneSection;
|
||||
if (!SdkConfig.get().disable_3pid_login) {
|
||||
if (threePidLogin && this._authStepIsUsed('m.login.msisdn')) {
|
||||
const phonePlaceholder = this._authStepIsRequired('m.login.msisdn') ?
|
||||
_t("Phone") :
|
||||
_t("Phone (optional)");
|
||||
|
@ -359,6 +379,7 @@ module.exports = React.createClass({
|
|||
<form onSubmit={this.onSubmit}>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
<input type="text" ref="username"
|
||||
autoFocus={true}
|
||||
placeholder={placeholderUsername} defaultValue={this.props.defaultUsername}
|
||||
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
||||
onBlur={this.onUsernameBlur} />
|
||||
|
@ -379,7 +400,7 @@ module.exports = React.createClass({
|
|||
{ phoneSection }
|
||||
</div>
|
||||
{_t(
|
||||
"Use an email address to receover your account. Other users " +
|
||||
"Use an email address to recover your account. Other users " +
|
||||
"can invite you to rooms using your contact details.",
|
||||
)}
|
||||
{ registerButton }
|
||||
|
|
47
src/components/views/auth/Welcome.js
Normal file
47
src/components/views/auth/Welcome.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
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 sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
|
||||
export default class Welcome extends React.PureComponent {
|
||||
render() {
|
||||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
|
||||
const LanguageSelector = sdk.getComponent('auth.LanguageSelector');
|
||||
|
||||
const pagesConfig = SdkConfig.get().embeddedPages;
|
||||
let pageUrl = null;
|
||||
if (pagesConfig) {
|
||||
pageUrl = pagesConfig.welcomeUrl;
|
||||
}
|
||||
if (!pageUrl) {
|
||||
pageUrl = 'welcome.html';
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
<div className="mx_Welcome">
|
||||
<EmbeddedPage className="mx_WelcomePage"
|
||||
url={pageUrl}
|
||||
/>
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
</AuthPage>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -158,7 +158,8 @@ module.exports = React.createClass({
|
|||
<BaseAvatar {...otherProps} name={roomName}
|
||||
idName={room ? room.roomId : null}
|
||||
urls={this.state.urls}
|
||||
onClick={this.props.viewAvatarOnClick ? this.onRoomAvatarClick : null} />
|
||||
onClick={this.props.viewAvatarOnClick ? this.onRoomAvatarClick : null}
|
||||
disabled={!this.state.urls[0]} />
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -19,6 +19,7 @@ import dis from '../../../dispatcher';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import LogoutDialog from "../dialogs/LogoutDialog";
|
||||
import Modal from "../../../Modal";
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
|
||||
export class TopLeftMenu extends React.Component {
|
||||
constructor() {
|
||||
|
@ -27,8 +28,28 @@ export class TopLeftMenu extends React.Component {
|
|||
this.signOut = this.signOut.bind(this);
|
||||
}
|
||||
|
||||
hasHomePage() {
|
||||
const config = SdkConfig.get();
|
||||
const pagesConfig = config.embeddedPages;
|
||||
if (pagesConfig && pagesConfig.homeUrl) {
|
||||
return true;
|
||||
}
|
||||
// This is a deprecated config option for the home page
|
||||
// (despite the name, given we also now have a welcome
|
||||
// page, which is not the same).
|
||||
return !!config.welcomePageUrl;
|
||||
}
|
||||
|
||||
render() {
|
||||
let homePageSection = null;
|
||||
if (this.hasHomePage()) {
|
||||
homePageSection = <ul className="mx_TopLeftMenu_section">
|
||||
<li className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>{_t("Home")}</li>
|
||||
</ul>;
|
||||
}
|
||||
|
||||
return <div className="mx_TopLeftMenu">
|
||||
{homePageSection}
|
||||
<ul className="mx_TopLeftMenu_section">
|
||||
<li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>{_t("Settings")}</li>
|
||||
</ul>
|
||||
|
@ -38,6 +59,11 @@ export class TopLeftMenu extends React.Component {
|
|||
</div>;
|
||||
}
|
||||
|
||||
viewHomePage() {
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
this.closeMenu();
|
||||
}
|
||||
|
||||
openSettings() {
|
||||
dis.dispatch({action: 'view_user_settings'});
|
||||
this.closeMenu();
|
||||
|
|
|
@ -22,7 +22,6 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
|||
import sdk from '../../../index';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
|
||||
|
||||
const MODE_LEGACY = 'legacy';
|
||||
|
@ -48,7 +47,7 @@ export default class DeviceVerifyDialog extends React.Component {
|
|||
this._showSasEvent = null;
|
||||
this.state = {
|
||||
phase: PHASE_START,
|
||||
mode: SettingsStore.isFeatureEnabled("feature_sas") ? MODE_SAS : MODE_LEGACY,
|
||||
mode: MODE_SAS,
|
||||
sasVerified: false,
|
||||
};
|
||||
}
|
||||
|
@ -241,7 +240,7 @@ export default class DeviceVerifyDialog extends React.Component {
|
|||
"and ask them whether the key they see in their User Settings " +
|
||||
"for this device matches the key below:") }
|
||||
</p>
|
||||
<div className="mx_UserSettings_cryptoSection">
|
||||
<div className="mx_DeviceVerifyDialog_cryptoSection">
|
||||
<ul>
|
||||
<li><label>{ _t("Device name") }:</label> <span>{ this.props.device.getDisplayName() }</span></li>
|
||||
<li><label>{ _t("Device ID") }:</label> <span><code>{ this.props.device.deviceId }</code></span></li>
|
||||
|
|
64
src/components/views/dialogs/InfoDialog.js
Normal file
64
src/components/views/dialogs/InfoDialog.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd.
|
||||
Copyright 2019 Bastian Masanek, Noxware IT <matrix@noxware.de>
|
||||
|
||||
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 React.createClass({
|
||||
displayName: 'InfoDialog',
|
||||
propTypes: {
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.node,
|
||||
button: PropTypes.string,
|
||||
onFinished: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: '',
|
||||
description: '',
|
||||
};
|
||||
},
|
||||
|
||||
onFinished: function() {
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return (
|
||||
<BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
contentId='mx_Dialog_content'
|
||||
hasCancel={false}
|
||||
>
|
||||
<div className="mx_Dialog_content" id="mx_Dialog_content">
|
||||
{ this.props.description }
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
onPrimaryButtonClick={this.onFinished}
|
||||
hasCancel={false}
|
||||
>
|
||||
</DialogButtons>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -77,7 +77,7 @@ export default React.createClass({
|
|||
<div role="alert">{ this.state.authError.message || this.state.authError.toString() }</div>
|
||||
<br />
|
||||
<AccessibleButton onClick={this._onDismissClick}
|
||||
className="mx_UserSettings_button"
|
||||
className="mx_GeneralButton"
|
||||
autoFocus="true"
|
||||
>
|
||||
{ _t("Dismiss") }
|
||||
|
|
|
@ -20,7 +20,6 @@ import sdk from '../../../index';
|
|||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export default class LogoutDialog extends React.Component {
|
||||
constructor() {
|
||||
|
@ -79,86 +78,59 @@ export default class LogoutDialog extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
let description;
|
||||
if (SettingsStore.isFeatureEnabled("feature_keybackup")) {
|
||||
description = <div>
|
||||
<p>{_t(
|
||||
"When you log out, you'll lose your secure message history. To prevent " +
|
||||
"this, set up a recovery method.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"Alternatively, advanced users can also manually export encryption keys in " +
|
||||
"<a>Settings</a> before logging out.", {},
|
||||
{
|
||||
a: sub => <a href='#/settings' onClick={this._onSettingsLinkClick}>{sub}</a>,
|
||||
},
|
||||
)}</p>
|
||||
</div>;
|
||||
} else {
|
||||
description = <div>{_t(
|
||||
"For security, logging out will delete any end-to-end " +
|
||||
"encryption keys from this browser. If you want to be able " +
|
||||
"to decrypt your conversation history from future Riot sessions, " +
|
||||
"please export your room keys for safe-keeping.",
|
||||
)}</div>;
|
||||
}
|
||||
const description = <div>
|
||||
<p>{_t(
|
||||
"When you log out, you'll lose your secure message history. To prevent " +
|
||||
"this, set up a recovery method.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"Alternatively, advanced users can also manually export encryption keys in " +
|
||||
"<a>Settings</a> before logging out.", {},
|
||||
{
|
||||
a: sub => <a href='#/settings' onClick={this._onSettingsLinkClick}>{sub}</a>,
|
||||
},
|
||||
)}</p>
|
||||
</div>;
|
||||
|
||||
if (SettingsStore.isFeatureEnabled("feature_keybackup")) {
|
||||
if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
// Not quite a standard question dialog as the primary button cancels
|
||||
// the action and does something else instead, whilst non-default button
|
||||
// confirms the action.
|
||||
return (<BaseDialog
|
||||
title={_t("Warning!")}
|
||||
contentId='mx_Dialog_content'
|
||||
if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
// Not quite a standard question dialog as the primary button cancels
|
||||
// the action and does something else instead, whilst non-default button
|
||||
// confirms the action.
|
||||
return (<BaseDialog
|
||||
title={_t("Warning!")}
|
||||
contentId='mx_Dialog_content'
|
||||
hasCancel={false}
|
||||
onFinsihed={this._onFinished}
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
{ description }
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t('Set a Recovery Method')}
|
||||
hasCancel={false}
|
||||
onFinsihed={this._onFinished}
|
||||
onPrimaryButtonClick={this._onSetRecoveryMethodClick}
|
||||
focus={true}
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
{ description }
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t('Set a Recovery Method')}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this._onSetRecoveryMethodClick}
|
||||
focus={true}
|
||||
>
|
||||
<button onClick={this._onLogoutConfirm}>
|
||||
{_t("I understand, log out without")}
|
||||
</button>
|
||||
</DialogButtons>
|
||||
</BaseDialog>);
|
||||
} else {
|
||||
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
|
||||
return (<QuestionDialog
|
||||
hasCancelButton={true}
|
||||
title={_t("Sign out")}
|
||||
// TODO: This is made up by me and would need to also mention verifying
|
||||
// once you can restorew a backup by verifying a device
|
||||
description={_t(
|
||||
"When signing in again, you can access encrypted chat history by " +
|
||||
"restoring your key backup. You'll need your recovery passphrase " +
|
||||
"or, if you didn't set a recovery passphrase, your recovery key " +
|
||||
"(that you downloaded).",
|
||||
)}
|
||||
button={_t("Sign out")}
|
||||
onFinished={this._onFinished}
|
||||
/>);
|
||||
}
|
||||
<button onClick={this._onLogoutConfirm}>
|
||||
{_t("I understand, log out without")}
|
||||
</button>
|
||||
</DialogButtons>
|
||||
</BaseDialog>);
|
||||
} else {
|
||||
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
|
||||
return (<QuestionDialog
|
||||
hasCancelButton={true}
|
||||
title={_t("Sign out")}
|
||||
description={description}
|
||||
// TODO: This is made up by me and would need to also mention verifying
|
||||
// once you can restore a backup by verifying a device
|
||||
description={_t(
|
||||
"When signing in again, you can access encrypted chat history by " +
|
||||
"restoring your key backup. You'll need your recovery passphrase " +
|
||||
"or, if you didn't set a recovery passphrase, your recovery key " +
|
||||
"(that you downloaded).",
|
||||
)}
|
||||
button={_t("Sign out")}
|
||||
extraButtons={[
|
||||
(<button key="export" className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}>
|
||||
{ _t("Export E2E room keys") }
|
||||
</button>),
|
||||
]}
|
||||
onFinished={this._onFinished}
|
||||
/>);
|
||||
}
|
||||
|
|
|
@ -19,27 +19,10 @@ import PropTypes from 'prop-types';
|
|||
import {Tab, TabbedView} from "../../structures/TabbedView";
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import AdvancedRoomSettingsTab from "../settings/tabs/AdvancedRoomSettingsTab";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import dis from '../../../dispatcher';
|
||||
import RolesRoomSettingsTab from "../settings/tabs/RolesRoomSettingsTab";
|
||||
import GeneralRoomSettingsTab from "../settings/tabs/GeneralRoomSettingsTab";
|
||||
import SecurityRoomSettingsTab from "../settings/tabs/SecurityRoomSettingsTab";
|
||||
|
||||
// TODO: Ditch this whole component
|
||||
export class TempTab extends React.Component {
|
||||
static propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
dis.dispatch({action: "open_old_room_settings"});
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello World</div>;
|
||||
}
|
||||
}
|
||||
import sdk from "../../../index";
|
||||
|
||||
export default class RoomSettingsDialog extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -47,19 +30,6 @@ export default class RoomSettingsDialog extends React.Component {
|
|||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount(): void {
|
||||
this.dispatcherRef = dis.register(this._onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
_onAction = (payload) => {
|
||||
if (payload.action !== 'close_room_settings') return;
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_getTabs() {
|
||||
const tabs = [];
|
||||
|
||||
|
@ -83,26 +53,20 @@ export default class RoomSettingsDialog extends React.Component {
|
|||
"mx_RoomSettingsDialog_warningIcon",
|
||||
<AdvancedRoomSettingsTab roomId={this.props.roomId} />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
_td("Visit old settings"),
|
||||
"mx_RoomSettingsDialog_warningIcon",
|
||||
<TempTab onClose={this.props.onFinished} />,
|
||||
));
|
||||
|
||||
return tabs;
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSettingsDialog">
|
||||
<div className="mx_SettingsDialog_header">
|
||||
{_t("Settings")}
|
||||
<span className="mx_SettingsDialog_close">
|
||||
<AccessibleButton className="mx_SettingsDialog_closeIcon" onClick={this.props.onFinished} />
|
||||
</span>
|
||||
<BaseDialog className='mx_RoomSettingsDialog' hasCancel={true}
|
||||
onFinished={this.props.onFinished} title={_t("Room Settings")}>
|
||||
<div className='ms_SettingsDialog_content'>
|
||||
<TabbedView tabs={this._getTabs()} />
|
||||
</div>
|
||||
<TabbedView tabs={this._getTabs()} />
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,8 +32,8 @@ function DeviceListEntry(props) {
|
|||
|
||||
return (
|
||||
<li>
|
||||
<DeviceVerifyButtons device={device} userId={userId} />
|
||||
{ device.deviceId }
|
||||
<DeviceVerifyButtons device={device} userId={userId} />
|
||||
<br />
|
||||
{ device.getDisplayName() }
|
||||
</li>
|
||||
|
|
|
@ -18,9 +18,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import {Tab, TabbedView} from "../../structures/TabbedView";
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import GeneralUserSettingsTab from "../settings/tabs/GeneralUserSettingsTab";
|
||||
import dis from '../../../dispatcher';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import LabsSettingsTab from "../settings/tabs/LabsSettingsTab";
|
||||
import SecuritySettingsTab from "../settings/tabs/SecuritySettingsTab";
|
||||
|
@ -29,22 +27,7 @@ import PreferencesSettingsTab from "../settings/tabs/PreferencesSettingsTab";
|
|||
import VoiceSettingsTab from "../settings/tabs/VoiceSettingsTab";
|
||||
import HelpSettingsTab from "../settings/tabs/HelpSettingsTab";
|
||||
import FlairSettingsTab from "../settings/tabs/FlairSettingsTab";
|
||||
|
||||
// TODO: Ditch this whole component
|
||||
export class TempTab extends React.Component {
|
||||
static propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
dis.dispatch({action: "view_old_user_settings"});
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello World</div>;
|
||||
}
|
||||
}
|
||||
import sdk from "../../../index";
|
||||
|
||||
export default class UserSettingsDialog extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -96,26 +79,20 @@ export default class UserSettingsDialog extends React.Component {
|
|||
"mx_UserSettingsDialog_helpIcon",
|
||||
<HelpSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
_td("Visit old settings"),
|
||||
"mx_UserSettingsDialog_helpIcon",
|
||||
<TempTab onClose={this.props.onFinished} />,
|
||||
));
|
||||
|
||||
return tabs;
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
return (
|
||||
<div className="mx_UserSettingsDialog">
|
||||
<div className="mx_SettingsDialog_header">
|
||||
{_t("Settings")}
|
||||
<span className="mx_SettingsDialog_close">
|
||||
<AccessibleButton className="mx_SettingsDialog_closeIcon" onClick={this.props.onFinished} />
|
||||
</span>
|
||||
<BaseDialog className='mx_UserSettingsDialog' hasCancel={true}
|
||||
onFinished={this.props.onFinished} title={_t("Settings")}>
|
||||
<div className='ms_SettingsDialog_content'>
|
||||
<TabbedView tabs={this._getTabs()} />
|
||||
</div>
|
||||
<TabbedView tabs={this._getTabs()} />
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,8 +19,13 @@ import sdk from '../../../../index';
|
|||
import MatrixClientPeg from '../../../../MatrixClientPeg';
|
||||
import Modal from '../../../../Modal';
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
import { _t } from '../../../../languageHandler';
|
||||
|
||||
const RESTORE_TYPE_PASSPHRASE = 0;
|
||||
const RESTORE_TYPE_RECOVERYKEY = 1;
|
||||
|
||||
/**
|
||||
* Dialog for restoring e2e keys from a backup and the user's recovery key
|
||||
*/
|
||||
|
@ -36,6 +41,7 @@ export default React.createClass({
|
|||
recoveryKeyValid: false,
|
||||
forceRecoveryKey: false,
|
||||
passPhrase: '',
|
||||
restoreType: null,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -80,10 +86,11 @@ export default React.createClass({
|
|||
this.setState({
|
||||
loading: true,
|
||||
restoreError: null,
|
||||
restoreType: RESTORE_TYPE_PASSPHRASE,
|
||||
});
|
||||
try {
|
||||
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword(
|
||||
this.state.passPhrase, undefined, undefined, this.state.backupInfo.version,
|
||||
this.state.passPhrase, undefined, undefined, this.state.backupInfo,
|
||||
);
|
||||
this.setState({
|
||||
loading: false,
|
||||
|
@ -102,10 +109,11 @@ export default React.createClass({
|
|||
this.setState({
|
||||
loading: true,
|
||||
restoreError: null,
|
||||
restoreType: RESTORE_TYPE_RECOVERYKEY,
|
||||
});
|
||||
try {
|
||||
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey(
|
||||
this.state.recoveryKey, undefined, undefined, this.state.backupInfo.version,
|
||||
this.state.recoveryKey, undefined, undefined, this.state.backupInfo,
|
||||
);
|
||||
this.setState({
|
||||
loading: false,
|
||||
|
@ -179,19 +187,31 @@ export default React.createClass({
|
|||
title = _t("Error");
|
||||
content = _t("Unable to load backup status");
|
||||
} else if (this.state.restoreError) {
|
||||
title = _t("Error");
|
||||
content = _t("Unable to restore backup");
|
||||
if (this.state.restoreError.errcode === MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY) {
|
||||
if (this.state.restoreType === RESTORE_TYPE_RECOVERYKEY) {
|
||||
title = _t("Recovery Key Mismatch");
|
||||
content = <div>
|
||||
<p>{_t(
|
||||
"Backup could not be decrypted with this key: " +
|
||||
"please verify that you entered the correct recovery key.",
|
||||
)}</p>
|
||||
</div>;
|
||||
} else {
|
||||
title = _t("Incorrect Recovery Passphrase");
|
||||
content = <div>
|
||||
<p>{_t(
|
||||
"Backup could not be decrypted with this passphrase: " +
|
||||
"please verify that you entered the correct recovery passphrase.",
|
||||
)}</p>
|
||||
</div>;
|
||||
}
|
||||
} else {
|
||||
title = _t("Error");
|
||||
content = _t("Unable to restore backup");
|
||||
}
|
||||
} else if (this.state.backupInfo === null) {
|
||||
title = _t("Error");
|
||||
content = _t("No backup found!");
|
||||
} else if (this.state.recoverInfo && this.state.recoverInfo.imported === 0) {
|
||||
title = _t("Error Restoring Backup");
|
||||
content = <div>
|
||||
<p>{_t(
|
||||
"Backup could not be decrypted with this key: " +
|
||||
"please verify that you entered the correct recovery key.",
|
||||
)}</p>
|
||||
</div>;
|
||||
} else if (this.state.recoverInfo) {
|
||||
title = _t("Backup Restored");
|
||||
let failedToDecrypt;
|
||||
|
|
|
@ -47,7 +47,7 @@ export default class AppPermission extends React.Component {
|
|||
return (
|
||||
<div className='mx_AppPermissionWarning'>
|
||||
<div className='mx_AppPermissionWarningImage'>
|
||||
<img src={require("../../../../res/img/warning.svg")} alt={_t('Warning!')} />
|
||||
<img src={require("../../../../res/img/feather-icons/warning-triangle.svg")} alt={_t('Warning!')} />
|
||||
</div>
|
||||
<div className='mx_AppPermissionWarningText'>
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{ _t('Do you want to load widget from URL:') }</span> <span className='mx_AppPermissionWarningTextURL'>{ this.state.curlBase }</span>
|
||||
|
|
|
@ -24,9 +24,9 @@ import PropTypes from 'prop-types';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import WidgetMessaging from '../../../WidgetMessaging';
|
||||
import TintableSvgButton from './TintableSvgButton';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import AppPermission from './AppPermission';
|
||||
import AppWarning from './AppWarning';
|
||||
|
@ -34,6 +34,7 @@ import MessageSpinner from './MessageSpinner';
|
|||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import dis from '../../../dispatcher';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||
const ENABLE_REACT_PERF = false;
|
||||
|
@ -51,6 +52,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._onSnapshotClick = this._onSnapshotClick.bind(this);
|
||||
this.onClickMenuBar = this.onClickMenuBar.bind(this);
|
||||
this._onMinimiseClick = this._onMinimiseClick.bind(this);
|
||||
|
@ -267,55 +269,59 @@ export default class AppTile extends React.Component {
|
|||
_onDeleteClick() {
|
||||
if (this.props.onDeleteClick) {
|
||||
this.props.onDeleteClick();
|
||||
} else {
|
||||
if (this._canUserModify()) {
|
||||
// Show delete confirmation dialog
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||
title: _t("Delete Widget"),
|
||||
description: _t(
|
||||
"Deleting a widget removes it for all users in this room." +
|
||||
" Are you sure you want to delete this widget?"),
|
||||
button: _t("Delete widget"),
|
||||
onFinished: (confirmed) => {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.setState({deleting: true});
|
||||
} else if (this._canUserModify()) {
|
||||
// Show delete confirmation dialog
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||
title: _t("Delete Widget"),
|
||||
description: _t(
|
||||
"Deleting a widget removes it for all users in this room." +
|
||||
" Are you sure you want to delete this widget?"),
|
||||
button: _t("Delete widget"),
|
||||
onFinished: (confirmed) => {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.setState({deleting: true});
|
||||
|
||||
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
||||
// its hold on the webcam. Without this, the widget holds a media
|
||||
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
|
||||
if (this.refs.appFrame) {
|
||||
// In practice we could just do `+= ''` to trick the browser
|
||||
// into thinking the URL changed, however I can foresee this
|
||||
// being optimized out by a browser. Instead, we'll just point
|
||||
// the iframe at a page that is reasonably safe to use in the
|
||||
// event the iframe doesn't wink away.
|
||||
// This is relative to where the Riot instance is located.
|
||||
this.refs.appFrame.src = 'about:blank';
|
||||
}
|
||||
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
||||
// its hold on the webcam. Without this, the widget holds a media
|
||||
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
|
||||
if (this.refs.appFrame) {
|
||||
// In practice we could just do `+= ''` to trick the browser
|
||||
// into thinking the URL changed, however I can foresee this
|
||||
// being optimized out by a browser. Instead, we'll just point
|
||||
// the iframe at a page that is reasonably safe to use in the
|
||||
// event the iframe doesn't wink away.
|
||||
// This is relative to where the Riot instance is located.
|
||||
this.refs.appFrame.src = 'about:blank';
|
||||
}
|
||||
|
||||
WidgetUtils.setRoomWidget(
|
||||
this.props.room.roomId,
|
||||
this.props.id,
|
||||
).catch((e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
WidgetUtils.setRoomWidget(
|
||||
this.props.room.roomId,
|
||||
this.props.id,
|
||||
).catch((e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
Modal.createTrackedDialog('Failed to remove widget', '', ErrorDialog, {
|
||||
title: _t('Failed to remove widget'),
|
||||
description: _t('An error ocurred whilst trying to remove the widget from the room'),
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({deleting: false});
|
||||
Modal.createTrackedDialog('Failed to remove widget', '', ErrorDialog, {
|
||||
title: _t('Failed to remove widget'),
|
||||
description: _t('An error ocurred whilst trying to remove the widget from the room'),
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.log("Revoke widget permissions - %s", this.props.id);
|
||||
this._revokeWidgetPermission();
|
||||
}
|
||||
}).finally(() => {
|
||||
this.setState({deleting: false});
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onCancelClick() {
|
||||
if (this.props.onDeleteClick) {
|
||||
this.props.onDeleteClick();
|
||||
} else {
|
||||
console.log("Revoke widget permissions - %s", this.props.id);
|
||||
this._revokeWidgetPermission();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -394,15 +400,6 @@ export default class AppTile extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
// Widget labels to render, depending upon user permissions
|
||||
// These strings are translated at the point that they are inserted in to the DOM, in the render method
|
||||
_deleteWidgetLabel() {
|
||||
if (this._canUserModify()) {
|
||||
return _td('Delete widget');
|
||||
}
|
||||
return _td('Revoke widget access');
|
||||
}
|
||||
|
||||
/* 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);
|
||||
|
@ -580,23 +577,14 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
|
||||
// editing is done in scalar
|
||||
const showEditButton = Boolean(this._scalarClient && this._canUserModify());
|
||||
const deleteWidgetLabel = this._deleteWidgetLabel();
|
||||
let deleteIcon = require("../../../../res/img/cancel_green.svg");
|
||||
let deleteClasses = 'mx_AppTileMenuBarWidget';
|
||||
if (this._canUserModify()) {
|
||||
deleteIcon = require("../../../../res/img/icon-delete-pink.svg");
|
||||
deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
|
||||
}
|
||||
|
||||
const canUserModify = this._canUserModify();
|
||||
const showEditButton = Boolean(this._scalarClient && canUserModify);
|
||||
const showDeleteButton = canUserModify;
|
||||
const showCancelButton = !showDeleteButton;
|
||||
// Picture snapshot - only show button when apps are maximised.
|
||||
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
|
||||
const showPictureSnapshotIcon = require("../../../../res/img/camera_green.svg");
|
||||
const popoutWidgetIcon = require("../../../../res/img/button-new-window.svg");
|
||||
const reloadWidgetIcon = require("../../../../res/img/button-refresh.svg");
|
||||
const minimizeIcon = require("../../../../res/img/minimize.svg");
|
||||
const maximizeIcon = require("../../../../res/img/maximize.svg");
|
||||
const windowStateIcon = (this.props.show ? minimizeIcon : maximizeIcon);
|
||||
const showMinimiseButton = this.props.showMinimise && this.props.show;
|
||||
const showMaximiseButton = this.props.showMinimise && !this.props.show;
|
||||
|
||||
let appTileClass;
|
||||
if (this.props.miniMode) {
|
||||
|
@ -607,71 +595,67 @@ export default class AppTile extends React.Component {
|
|||
appTileClass = 'mx_AppTile';
|
||||
}
|
||||
|
||||
const menuBarClasses = classNames({
|
||||
mx_AppTileMenuBar: true,
|
||||
mx_AppTileMenuBar_expanded: this.props.show,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={appTileClass} id={this.props.id}>
|
||||
{ this.props.showMenubar &&
|
||||
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||
<div ref="menu_bar" className={menuBarClasses} onClick={this.onClickMenuBar}>
|
||||
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
||||
{ this.props.showMinimise && <TintableSvgButton
|
||||
src={windowStateIcon}
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
{ /* Minimise widget */ }
|
||||
{ showMinimiseButton && <AccessibleButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_minimise"
|
||||
title={_t('Minimize apps')}
|
||||
width="10"
|
||||
height="10"
|
||||
onClick={this._onMinimiseClick}
|
||||
/> }
|
||||
{ /* Maximise widget */ }
|
||||
{ showMaximiseButton && <AccessibleButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_maximise"
|
||||
title={_t('Minimize apps')}
|
||||
onClick={this._onMinimiseClick}
|
||||
/> }
|
||||
{ /* Title */ }
|
||||
{ this.props.showTitle && this._getTileTitle() }
|
||||
</span>
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{ /* Reload widget */ }
|
||||
{ this.props.showReload && <TintableSvgButton
|
||||
src={reloadWidgetIcon}
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
{ this.props.showReload && <AccessibleButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_reload"
|
||||
title={_t('Reload widget')}
|
||||
onClick={this._onReloadWidgetClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
|
||||
{ /* Popout widget */ }
|
||||
{ this.props.showPopout && <TintableSvgButton
|
||||
src={popoutWidgetIcon}
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
{ this.props.showPopout && <AccessibleButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
|
||||
title={_t('Popout widget')}
|
||||
onClick={this._onPopoutWidgetClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
|
||||
{ /* Snapshot widget */ }
|
||||
{ showPictureSnapshotButton && <TintableSvgButton
|
||||
src={showPictureSnapshotIcon}
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
{ showPictureSnapshotButton && <AccessibleButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_snapshot"
|
||||
title={_t('Picture')}
|
||||
onClick={this._onSnapshotClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
|
||||
{ /* Edit widget */ }
|
||||
{ showEditButton && <TintableSvgButton
|
||||
src={require("../../../../res/img/edit_green.svg")}
|
||||
className={"mx_AppTileMenuBarWidget " +
|
||||
(this.props.showDelete ? "mx_AppTileMenuBarWidgetPadding" : "")}
|
||||
{ showEditButton && <AccessibleButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_edit"
|
||||
title={_t('Edit')}
|
||||
onClick={this._onEditClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
|
||||
{ /* Delete widget */ }
|
||||
{ this.props.showDelete && <TintableSvgButton
|
||||
src={deleteIcon}
|
||||
className={deleteClasses}
|
||||
title={_t(deleteWidgetLabel)}
|
||||
{ showDeleteButton && <AccessibleButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_delete"
|
||||
title={_t('Delete widget')}
|
||||
onClick={this._onDeleteClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
{ /* Cancel widget */ }
|
||||
{ showCancelButton && <AccessibleButton
|
||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_cancel"
|
||||
title={_t('Revoke widget access')}
|
||||
onClick={this._onCancelClick}
|
||||
/> }
|
||||
</span>
|
||||
</div> }
|
||||
|
|
|
@ -20,12 +20,12 @@ import PropTypes from 'prop-types';
|
|||
import {emojifyText, containsEmoji} from '../../../HtmlUtils';
|
||||
|
||||
export default function EmojiText(props) {
|
||||
const {element, children, ...restProps} = props;
|
||||
const {element, children, addAlt, ...restProps} = props;
|
||||
|
||||
// fast path: simple regex to detect strings that don't contain
|
||||
// emoji and just return them
|
||||
if (containsEmoji(children)) {
|
||||
restProps.dangerouslySetInnerHTML = emojifyText(children);
|
||||
restProps.dangerouslySetInnerHTML = emojifyText(children, addAlt);
|
||||
return React.createElement(element, restProps);
|
||||
} else {
|
||||
return React.createElement(element, restProps, children);
|
||||
|
@ -39,4 +39,5 @@ EmojiText.propTypes = {
|
|||
|
||||
EmojiText.defaultProps = {
|
||||
element: 'span',
|
||||
addAlt: true,
|
||||
};
|
||||
|
|
|
@ -1,103 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd.
|
||||
|
||||
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 classnames from 'classnames';
|
||||
|
||||
import sdk from '../../../index';
|
||||
|
||||
class HexVerifyPair extends React.Component {
|
||||
static propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
index: PropTypes.number,
|
||||
verified: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
_onClick = () => {
|
||||
this.setState({verified: !this.props.verified});
|
||||
this.props.onChange(this.props.index, !this.props.verified);
|
||||
}
|
||||
|
||||
render() {
|
||||
const classNames = {
|
||||
mx_HexVerify_pair: true,
|
||||
mx_HexVerify_pair_verified: this.props.verified,
|
||||
};
|
||||
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
||||
return <AccessibleButton className={classnames(classNames)}
|
||||
onClick={this._onClick}
|
||||
>
|
||||
{this.props.text}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helps a user verify a hexadecimal code matches one displayed
|
||||
* elsewhere (eg. on a different device)
|
||||
*/
|
||||
export default class HexVerify extends React.Component {
|
||||
static propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
onVerifiedStateChange: PropTypes.func,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onVerifiedStateChange: function() {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
pairsVerified: [],
|
||||
};
|
||||
for (let i = 0; i < props.text.length; i += 2) {
|
||||
this.state.pairsVerified.push(false);
|
||||
}
|
||||
}
|
||||
|
||||
_onPairChange = (index, newVal) => {
|
||||
const oldVerified = this.state.pairsVerified.reduce((acc, val) => {
|
||||
return acc && val;
|
||||
}, true);
|
||||
const newPairsVerified = this.state.pairsVerified.slice(0);
|
||||
newPairsVerified[index] = newVal;
|
||||
const newVerified = newPairsVerified.reduce((acc, val) => {
|
||||
return acc && val;
|
||||
}, true);
|
||||
this.setState({pairsVerified: newPairsVerified});
|
||||
if (oldVerified !== newVerified) {
|
||||
this.props.onVerifiedStateChange(newVerified);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const pairs = [];
|
||||
|
||||
for (let i = 0; i < this.props.text.length / 2; ++i) {
|
||||
pairs.push(<HexVerifyPair key={i} index={i}
|
||||
text={this.props.text.substr(i * 2, 2)}
|
||||
verified={this.state.pairsVerified[i]}
|
||||
onChange={this._onPairChange}
|
||||
/>);
|
||||
}
|
||||
return <div className="mx_HexVerify">
|
||||
{pairs}
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -76,7 +76,7 @@ export default class ManageIntegsButton extends React.Component {
|
|||
if (this.scalarClient !== null) {
|
||||
const integrationsButtonClasses = classNames({
|
||||
mx_RoomHeader_button: true,
|
||||
mx_RoomSettings_integrationsButton_error: !!this.state.scalarError,
|
||||
mx_ManageIntegsButton_error: !!this.state.scalarError,
|
||||
});
|
||||
|
||||
if (this.state.scalarError && !this.scalarClient.hasCredentials()) {
|
||||
|
@ -87,7 +87,7 @@ export default class ManageIntegsButton extends React.Component {
|
|||
/>;
|
||||
// Popup shown when hovering over integrationsButton_error (via CSS)
|
||||
integrationsErrorPopup = (
|
||||
<span className="mx_RoomSettings_integrationsButton_errorPopup">
|
||||
<span className="mx_ManageIntegsButton_errorPopup">
|
||||
{ _t('Could not connect to the integration server') }
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -23,9 +23,11 @@ import sdk from '../../../index';
|
|||
import dis from '../../../dispatcher';
|
||||
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
|
||||
import * as ContextualMenu from '../../structures/ContextualMenu';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import TagOrderStore from '../../../stores/TagOrderStore';
|
||||
|
||||
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
|
||||
// a thing to click on for the user to filter the visible rooms in the RoomList to:
|
||||
|
@ -168,6 +170,16 @@ export default React.createClass({
|
|||
mx_TagTile_selected: this.props.selected,
|
||||
});
|
||||
|
||||
const badge = TagOrderStore.getGroupBadge(this.props.tag);
|
||||
let badgeElement;
|
||||
if (badge && !this.state.hover) {
|
||||
const badgeClasses = classNames({
|
||||
"mx_TagTile_badge": true,
|
||||
"mx_TagTile_badgeHighlight": badge.highlight,
|
||||
});
|
||||
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>);
|
||||
}
|
||||
|
||||
const tip = this.state.hover ?
|
||||
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
|
||||
<div />;
|
||||
|
@ -186,6 +198,7 @@ export default React.createClass({
|
|||
/>
|
||||
{ tip }
|
||||
{ contextButton }
|
||||
{ badgeElement }
|
||||
</div>
|
||||
</AccessibleButton>;
|
||||
},
|
||||
|
|
|
@ -38,6 +38,12 @@ export default class ToggleSwitch extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.checked !== this.state.checked) {
|
||||
this.setState({checked: nextProps.checked});
|
||||
}
|
||||
}
|
||||
|
||||
_onClick = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
|
|
@ -22,9 +22,6 @@ import ReactDOM from 'react-dom';
|
|||
import PropTypes from 'prop-types';
|
||||
import highlight from 'highlight.js';
|
||||
import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import * as linkify from 'linkifyjs';
|
||||
import linkifyElement from 'linkifyjs/element';
|
||||
import linkifyMatrix from '../../../linkify-matrix';
|
||||
import sdk from '../../../index';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -38,8 +35,6 @@ import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
|||
import ReplyThread from "../elements/ReplyThread";
|
||||
import {host as matrixtoHost} from '../../../matrix-to';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'TextualBody',
|
||||
|
||||
|
@ -98,7 +93,7 @@ module.exports = React.createClass({
|
|||
// are still sent as plaintext URLs. If these are ever pillified in the composer,
|
||||
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
|
||||
this.pillifyLinks(this.refs.content.children);
|
||||
linkifyElement(this.refs.content, linkifyMatrix.options);
|
||||
HtmlUtils.linkifyElement(this.refs.content);
|
||||
this.calculateUrlPreview();
|
||||
|
||||
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
|
||||
|
|
|
@ -256,16 +256,16 @@ module.exports = React.createClass({
|
|||
if (this.state.remoteDomains.length) {
|
||||
remote_aliases_section = (
|
||||
<div>
|
||||
<div className="mx_RoomSettings_aliasLabel">
|
||||
<div>
|
||||
{ _t("Remote addresses for this room:") }
|
||||
</div>
|
||||
<div className="mx_RoomSettings_aliasesTable">
|
||||
<div>
|
||||
{ this.state.remoteDomains.map((domain, i) => {
|
||||
return this.state.domainToAliases[domain].map(function(alias, j) {
|
||||
return (
|
||||
<div className="mx_RoomSettings_aliasesTableRow" key={i + "_" + j}>
|
||||
<div key={i + "_" + j}>
|
||||
<EditableText
|
||||
className="mx_RoomSettings_alias mx_RoomSettings_editable"
|
||||
className="mx_AliasSettings_alias mx_AliasSettings_editable"
|
||||
blurToCancel={false}
|
||||
editable={false}
|
||||
initialValue={alias} />
|
||||
|
|
|
@ -39,6 +39,10 @@ const ROOM_COLORS = [
|
|||
//["#595959", "#ececec"], // Grey makes everything appear disabled, so remove it for now
|
||||
];
|
||||
|
||||
// Dev note: this component is not attached anywhere, but is left here as it
|
||||
// has a high possibility of being used in the nearish future.
|
||||
// Ref: https://github.com/vector-im/riot-web/issues/8421
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ColorSettings',
|
||||
|
||||
|
@ -125,24 +129,24 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_RoomSettings_roomColors">
|
||||
<div className="mx_ColorSettings_roomColors">
|
||||
{ ROOM_COLORS.map((room_color, i) => {
|
||||
let selected;
|
||||
if (i === this.state.index) {
|
||||
selected = (
|
||||
<div className="mx_RoomSettings_roomColor_selected">
|
||||
<div className="mx_ColorSettings_roomColor_selected">
|
||||
<img src={require("../../../../res/img/tick.svg")} width="17" height="14" alt="./" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const boundClick = this._onColorSchemeChanged.bind(this, i);
|
||||
return (
|
||||
<div className="mx_RoomSettings_roomColor"
|
||||
<div className="mx_ColorSettings_roomColor"
|
||||
key={"room_color_" + i}
|
||||
style={{ backgroundColor: room_color[1] }}
|
||||
onClick={boundClick}>
|
||||
{ selected }
|
||||
<div className="mx_RoomSettings_roomColorPrimary" style={{ backgroundColor: room_color[0] }}></div>
|
||||
<div className="mx_ColorSettings_roomColorPrimary" style={{ backgroundColor: room_color[0] }}></div>
|
||||
</div>
|
||||
);
|
||||
}) }
|
||||
|
|
39
src/components/views/rooms/E2EIcon.js
Normal file
39
src/components/views/rooms/E2EIcon.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
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 classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default function(props) {
|
||||
const isWarning = props.status === "warning";
|
||||
const isVerified = props.status === "verified";
|
||||
const e2eIconClasses = classNames({
|
||||
mx_E2EIcon: true,
|
||||
mx_E2EIcon_warning: isWarning,
|
||||
mx_E2EIcon_verified: isVerified,
|
||||
}, props.className);
|
||||
let e2eTitle;
|
||||
if (isWarning) {
|
||||
e2eTitle = props.isUser ?
|
||||
_t("Some devices for this user are not trusted") :
|
||||
_t("Some devices in this encrypted room are not trusted");
|
||||
} else if (isVerified) {
|
||||
e2eTitle = props.isUser ?
|
||||
_t("All devices for this user are trusted") :
|
||||
_t("All devices in this encrypted room are trusted");
|
||||
}
|
||||
return (<div className={e2eIconClasses} title={e2eTitle} />);
|
||||
}
|
|
@ -63,6 +63,8 @@ const stateEventTileTypes = {
|
|||
'm.room.server_acl': 'messages.TextualEvent',
|
||||
'im.vector.modular.widgets': 'messages.TextualEvent',
|
||||
'm.room.tombstone': 'messages.TextualEvent',
|
||||
'm.room.join_rules': 'messages.TextualEvent',
|
||||
'm.room.guest_access': 'messages.TextualEvent',
|
||||
};
|
||||
|
||||
function getHandlerTile(ev) {
|
||||
|
@ -459,17 +461,21 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
// event is encrypted, display padlock corresponding to whether or not it is verified
|
||||
if (ev.isEncrypted()) {
|
||||
return this.state.verified ? <E2ePadlockVerified {...props} /> : <E2ePadlockUnverified {...props} />;
|
||||
if (this.state.verified) {
|
||||
return; // no icon for verified
|
||||
} else {
|
||||
return (<E2ePadlockUnverified {...props} />);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.matrixClient.isRoomEncrypted(ev.getRoomId())) {
|
||||
// else if room is encrypted
|
||||
// and event is being encrypted or is not_sent (Unknown Devices/Network Error)
|
||||
if (ev.status === EventStatus.ENCRYPTING) {
|
||||
return <E2ePadlockEncrypting {...props} />;
|
||||
return;
|
||||
}
|
||||
if (ev.status === EventStatus.NOT_SENT) {
|
||||
return <E2ePadlockNotSent {...props} />;
|
||||
return;
|
||||
}
|
||||
// if the event is not encrypted, but it's an e2e room, show the open padlock
|
||||
return <E2ePadlockUnencrypted {...props} />;
|
||||
|
@ -767,57 +773,29 @@ module.exports.haveTileForEvent = function(e) {
|
|||
|
||||
function E2ePadlockUndecryptable(props) {
|
||||
return (
|
||||
<E2ePadlock alt={_t("Undecryptable")}
|
||||
src={require("../../../../res/img/e2e-blocked.svg")} width="12" height="12"
|
||||
style={{ marginLeft: "-1px" }} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function E2ePadlockEncrypting(props) {
|
||||
return (
|
||||
<E2ePadlock alt={_t("Encrypting")}
|
||||
src={require("../../../../res/img/e2e-encrypting.svg")} width="10" height="12"
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function E2ePadlockNotSent(props) {
|
||||
return (
|
||||
<E2ePadlock alt={_t("Encrypted, not sent")}
|
||||
src={require("../../../../res/img/e2e-not_sent.svg")} width="10" height="12"
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function E2ePadlockVerified(props) {
|
||||
return (
|
||||
<E2ePadlock alt={_t("Encrypted by a verified device")}
|
||||
src={require("../../../../res/img/e2e-verified.svg")} width="10" height="12"
|
||||
{...props} />
|
||||
<E2ePadlock title={_t("Undecryptable")} icon="undecryptable" />
|
||||
);
|
||||
}
|
||||
|
||||
function E2ePadlockUnverified(props) {
|
||||
return (
|
||||
<E2ePadlock alt={_t("Encrypted by an unverified device")}
|
||||
src={require("../../../../res/img/e2e-warning.svg")} width="15" height="12"
|
||||
style={{ marginLeft: "-2px" }} {...props} />
|
||||
<E2ePadlock title={_t("Encrypted by an unverified device")} icon="unverified" />
|
||||
);
|
||||
}
|
||||
|
||||
function E2ePadlockUnencrypted(props) {
|
||||
return (
|
||||
<E2ePadlock alt={_t("Unencrypted message")}
|
||||
src={require("../../../../res/img/e2e-unencrypted.svg")} width="12" height="12"
|
||||
{...props} />
|
||||
<E2ePadlock title={_t("Unencrypted message")} icon="unencrypted" />
|
||||
);
|
||||
}
|
||||
|
||||
function E2ePadlock(props) {
|
||||
if (SettingsStore.getValue("alwaysShowEncryptionIcons")) {
|
||||
return <img className="mx_EventTile_e2eIcon" {...props} />;
|
||||
return <div
|
||||
className={`mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${props.icon}`}
|
||||
title={props.title} onClick={props.onClick} />;
|
||||
} else {
|
||||
return <img className="mx_EventTile_e2eIcon mx_EventTile_e2eIcon_hidden" {...props} />;
|
||||
return <div className="mx_EventTile_e2eIcon mx_EventTile_e2eIcon_hidden" onClick={props.onClick} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,19 +16,15 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { linkifyElement } from '../../../HtmlUtils';
|
||||
|
||||
const sdk = require('../../../index');
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const ImageUtils = require('../../../ImageUtils');
|
||||
const Modal = require('../../../Modal');
|
||||
|
||||
const linkify = require('linkifyjs');
|
||||
const linkifyElement = require('linkifyjs/element');
|
||||
const linkifyMatrix = require('../../../linkify-matrix');
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'LinkPreviewWidget',
|
||||
|
||||
|
@ -62,13 +58,13 @@ module.exports = React.createClass({
|
|||
|
||||
componentDidMount: function() {
|
||||
if (this.refs.description) {
|
||||
linkifyElement(this.refs.description, linkifyMatrix.options);
|
||||
linkifyElement(this.refs.description);
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
if (this.refs.description) {
|
||||
linkifyElement(this.refs.description, linkifyMatrix.options);
|
||||
linkifyElement(this.refs.description);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -18,32 +18,18 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class MemberDeviceInfo extends React.Component {
|
||||
render() {
|
||||
let indicator = null;
|
||||
const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
|
||||
|
||||
if (this.props.device.isBlocked()) {
|
||||
indicator = (
|
||||
<div className="mx_MemberDeviceInfo_blacklisted">
|
||||
<img src={require("../../../../res/img/e2e-blocked.svg")} width="12" height="12" style={{ marginLeft: "-1px" }} alt={_t("Blacklisted")} />
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.device.isVerified()) {
|
||||
indicator = (
|
||||
<div className="mx_MemberDeviceInfo_verified">
|
||||
<img src={require("../../../../res/img/e2e-verified.svg")} width="10" height="12" alt={_t("Verified")} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
indicator = (
|
||||
<div className="mx_MemberDeviceInfo_unverified">
|
||||
<img src={require("../../../../res/img/e2e-warning.svg")} width="15" height="12" style={{ marginLeft: "-2px" }} alt={_t("Unverified")} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const iconClasses = classNames({
|
||||
mx_MemberDeviceInfo_icon: true,
|
||||
mx_MemberDeviceInfo_icon_blacklisted: this.props.device.isBlocked(),
|
||||
mx_MemberDeviceInfo_icon_verified: this.props.device.isVerified(),
|
||||
mx_MemberDeviceInfo_icon_unverified: this.props.device.isUnverified(),
|
||||
});
|
||||
const indicator = (<div className={iconClasses} />);
|
||||
const deviceName = this.props.device.ambiguous ?
|
||||
(this.props.device.getDisplayName() ? this.props.device.getDisplayName() : "") + " (" + this.props.device.deviceId + ")" :
|
||||
this.props.device.getDisplayName();
|
||||
|
@ -52,10 +38,10 @@ export default class MemberDeviceInfo extends React.Component {
|
|||
return (
|
||||
<div className="mx_MemberDeviceInfo"
|
||||
title={_t("device id: ") + this.props.device.deviceId} >
|
||||
{ indicator }
|
||||
<div className="mx_MemberDeviceInfo_deviceInfo">
|
||||
<div className="mx_MemberDeviceInfo_deviceId">
|
||||
{ deviceName }
|
||||
{ indicator }
|
||||
</div>
|
||||
</div>
|
||||
<DeviceVerifyButtons userId={this.props.userId} device={this.props.device} />
|
||||
|
|
|
@ -43,6 +43,7 @@ import RoomViewStore from '../../../stores/RoomViewStore';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import MultiInviter from "../../../utils/MultiInviter";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import E2EIcon from "./E2EIcon";
|
||||
|
||||
module.exports = withMatrixClient(React.createClass({
|
||||
displayName: 'MemberInfo',
|
||||
|
@ -153,11 +154,19 @@ module.exports = withMatrixClient(React.createClass({
|
|||
// Promise.resolve to handle transition from static result to promise; can be removed
|
||||
// in future
|
||||
Promise.resolve(this.props.matrixClient.getStoredDevicesForUser(userId)).then((devices) => {
|
||||
this.setState({devices: devices});
|
||||
this.setState({
|
||||
devices: devices,
|
||||
e2eStatus: this._getE2EStatus(devices),
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_getE2EStatus: function(devices) {
|
||||
const hasUnverifiedDevice = devices.some((device) => device.isUnverified());
|
||||
return hasUnverifiedDevice ? "warning" : "verified";
|
||||
},
|
||||
|
||||
onRoom: function(room) {
|
||||
this.forceUpdate();
|
||||
},
|
||||
|
@ -234,8 +243,13 @@ module.exports = withMatrixClient(React.createClass({
|
|||
// we got cancelled - presumably a different user now
|
||||
return;
|
||||
}
|
||||
|
||||
self._disambiguateDevices(devices);
|
||||
self.setState({devicesLoading: false, devices: devices});
|
||||
self.setState({
|
||||
devicesLoading: false,
|
||||
devices: devices,
|
||||
e2eStatus: self._getE2EStatus(devices),
|
||||
});
|
||||
}, function(err) {
|
||||
console.log("Error downloading devices", err);
|
||||
self.setState({devicesLoading: false});
|
||||
|
@ -965,6 +979,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}>
|
||||
<img src={require("../../../../res/img/minimise.svg")} width="10" height="16" className="mx_filterFlipColor" alt={_t('Close')} />
|
||||
</AccessibleButton>
|
||||
{ this.state.e2eStatus ? <E2EIcon status={this.state.e2eStatus} isUser={true} /> : undefined }
|
||||
<EmojiText element="h2">{ memberName }</EmojiText>
|
||||
</div>
|
||||
{ avatarElement }
|
||||
|
|
|
@ -26,6 +26,9 @@ import RoomViewStore from '../../../stores/RoomViewStore';
|
|||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import Stickerpicker from './Stickerpicker';
|
||||
import { makeRoomPermalink } from '../../../matrix-to';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import E2EIcon from './E2EIcon';
|
||||
|
||||
const formatButtonList = [
|
||||
_td("bold"),
|
||||
|
@ -316,25 +319,14 @@ export default class MessageComposer extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
let e2eImg; let e2eTitle; let e2eClass;
|
||||
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||
if (roomIsEncrypted) {
|
||||
// FIXME: show a /!\ if there are untrusted devices in the room...
|
||||
e2eImg = require("../../../../res/img/e2e-verified.svg");
|
||||
e2eTitle = _t('Encrypted room');
|
||||
e2eClass = 'mx_MessageComposer_e2eIcon';
|
||||
} else {
|
||||
e2eImg = require("../../../../res/img/e2e-unencrypted.svg");
|
||||
e2eTitle = _t('Unencrypted room');
|
||||
e2eClass = 'mx_MessageComposer_e2eIcon mx_filterFlipColor';
|
||||
if (this.props.e2eStatus) {
|
||||
controls.push(<E2EIcon
|
||||
status={this.props.e2eStatus}
|
||||
key="e2eIcon"
|
||||
className="mx_MessageComposer_e2eIcon" />
|
||||
);
|
||||
}
|
||||
|
||||
controls.push(
|
||||
<img key="e2eIcon" className={e2eClass} src={e2eImg} width="12" height="12"
|
||||
alt={e2eTitle} title={e2eTitle}
|
||||
/>,
|
||||
);
|
||||
|
||||
let callButton;
|
||||
let videoCallButton;
|
||||
let hangupButton;
|
||||
|
@ -413,6 +405,7 @@ export default class MessageComposer extends React.Component {
|
|||
key="controls_formatting" />
|
||||
) : null;
|
||||
|
||||
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||
let placeholderText;
|
||||
if (this.state.isQuoting) {
|
||||
if (roomIsEncrypted) {
|
||||
|
@ -509,9 +502,13 @@ export default class MessageComposer extends React.Component {
|
|||
</div>;
|
||||
}
|
||||
|
||||
const wrapperClasses = classNames({
|
||||
mx_MessageComposer_wrapper: true,
|
||||
mx_MessageComposer_hasE2EIcon: !!this.props.e2eStatus,
|
||||
});
|
||||
return (
|
||||
<div className="mx_MessageComposer">
|
||||
<div className="mx_MessageComposer_wrapper">
|
||||
<div className={wrapperClasses}>
|
||||
<div className="mx_MessageComposer_row">
|
||||
{ controls }
|
||||
</div>
|
||||
|
|
|
@ -17,15 +17,11 @@ limitations under the License.
|
|||
import sdk from '../../../index';
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as linkify from 'linkifyjs';
|
||||
import linkifyElement from 'linkifyjs/element';
|
||||
import linkifyMatrix from '../../../linkify-matrix';
|
||||
import { linkifyElement } from '../../../HtmlUtils';
|
||||
import { ContentRepo } from 'matrix-js-sdk';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
export function getDisplayAliasForRoom(room) {
|
||||
return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
|
||||
}
|
||||
|
@ -53,7 +49,7 @@ export default React.createClass({
|
|||
|
||||
_linkifyTopic: function() {
|
||||
if (this.refs.topic) {
|
||||
linkifyElement(this.refs.topic, linkifyMatrix.options);
|
||||
linkifyElement(this.refs.topic);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -25,16 +25,13 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
|||
import Modal from "../../../Modal";
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
|
||||
import * as linkify from 'linkifyjs';
|
||||
import linkifyElement from 'linkifyjs/element';
|
||||
import linkifyMatrix from '../../../linkify-matrix';
|
||||
import { linkifyElement } from '../../../HtmlUtils';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import ManageIntegsButton from '../elements/ManageIntegsButton';
|
||||
import {CancelButton} from './SimpleRoomHeader';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
import E2EIcon from './E2EIcon';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomHeader',
|
||||
|
@ -42,23 +39,20 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
room: PropTypes.object,
|
||||
oobData: PropTypes.object,
|
||||
editing: PropTypes.bool,
|
||||
saving: PropTypes.bool,
|
||||
inRoom: PropTypes.bool,
|
||||
collapsedRhs: PropTypes.bool,
|
||||
onSettingsClick: PropTypes.func,
|
||||
onPinnedClick: PropTypes.func,
|
||||
onSaveClick: PropTypes.func,
|
||||
onSearchClick: PropTypes.func,
|
||||
onLeaveClick: PropTypes.func,
|
||||
onCancelClick: PropTypes.func,
|
||||
e2eStatus: PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
editing: false,
|
||||
inRoom: false,
|
||||
onSaveClick: function() {},
|
||||
onCancelClick: null,
|
||||
};
|
||||
},
|
||||
|
@ -78,7 +72,7 @@ module.exports = React.createClass({
|
|||
|
||||
componentDidUpdate: function() {
|
||||
if (this.refs.topic) {
|
||||
linkifyElement(this.refs.topic, linkifyMatrix.options);
|
||||
linkifyElement(this.refs.topic);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -118,33 +112,6 @@ module.exports = React.createClass({
|
|||
this.forceUpdate();
|
||||
},
|
||||
|
||||
onAvatarPickerClick: function(ev) {
|
||||
if (this.refs.file_label) {
|
||||
this.refs.file_label.click();
|
||||
}
|
||||
},
|
||||
|
||||
onAvatarSelected: function(ev) {
|
||||
const changeAvatar = this.refs.changeAvatar;
|
||||
if (!changeAvatar) {
|
||||
console.error("No ChangeAvatar found to upload image to!");
|
||||
return;
|
||||
}
|
||||
changeAvatar.onFileSelected(ev).catch(function(err) {
|
||||
const errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to set avatar: " + errMsg);
|
||||
Modal.createTrackedDialog('Failed to set avatar', '', ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
description: _t("Failed to set avatar."),
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
||||
onAvatarRemoveClick: function() {
|
||||
MatrixClientPeg.get().sendStateEvent(this.props.room.roomId, 'm.room.avatar', {url: null}, '');
|
||||
},
|
||||
|
||||
onShareRoomClick: function(ev) {
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room dialog', '', ShareDialog, {
|
||||
|
@ -178,160 +145,78 @@ module.exports = React.createClass({
|
|||
return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* After editing the settings, get the new name for the room
|
||||
*
|
||||
* @return {?string} newName or undefined if we didn't let the user edit the room name
|
||||
*/
|
||||
getEditedName: function() {
|
||||
let newName;
|
||||
if (this.refs.nameEditor) {
|
||||
newName = this.refs.nameEditor.getRoomName();
|
||||
}
|
||||
return newName;
|
||||
},
|
||||
|
||||
/**
|
||||
* After editing the settings, get the new topic for the room
|
||||
*
|
||||
* @return {?string} newTopic or undefined if we didn't let the user edit the room topic
|
||||
*/
|
||||
getEditedTopic: function() {
|
||||
let newTopic;
|
||||
if (this.refs.topicEditor) {
|
||||
newTopic = this.refs.topicEditor.getTopic();
|
||||
}
|
||||
return newTopic;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
||||
const ChangeAvatar = sdk.getComponent("settings.ChangeAvatar");
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
let name = null;
|
||||
let searchStatus = null;
|
||||
let topicElement = null;
|
||||
let cancelButton = null;
|
||||
let spinner = null;
|
||||
let saveButton = null;
|
||||
let settingsButton = null;
|
||||
let pinnedEventsButton = null;
|
||||
|
||||
let canSetRoomName;
|
||||
let canSetRoomAvatar;
|
||||
let canSetRoomTopic;
|
||||
if (this.props.editing) {
|
||||
// calculate permissions. XXX: this should be done on mount or something
|
||||
const userId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
canSetRoomName = this.props.room.currentState.maySendStateEvent('m.room.name', userId);
|
||||
canSetRoomAvatar = this.props.room.currentState.maySendStateEvent('m.room.avatar', userId);
|
||||
canSetRoomTopic = this.props.room.currentState.maySendStateEvent('m.room.topic', userId);
|
||||
|
||||
saveButton = (
|
||||
<AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>
|
||||
{ _t("Save") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
const e2eIcon = this.props.e2eStatus ?
|
||||
<E2EIcon status={this.props.e2eStatus} /> :
|
||||
undefined;
|
||||
|
||||
if (this.props.onCancelClick) {
|
||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||
}
|
||||
|
||||
if (this.props.saving) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
spinner = <div className="mx_RoomHeader_spinner"><Spinner /></div>;
|
||||
// don't display the search count until the search completes and
|
||||
// gives us a valid (possibly zero) searchCount.
|
||||
if (this.props.searchInfo &&
|
||||
this.props.searchInfo.searchCount !== undefined &&
|
||||
this.props.searchInfo.searchCount !== null) {
|
||||
searchStatus = <div className="mx_RoomHeader_searchStatus">
|
||||
{ _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (canSetRoomName) {
|
||||
const RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
|
||||
name = <RoomNameEditor ref="nameEditor" room={this.props.room} />;
|
||||
} else {
|
||||
// don't display the search count until the search completes and
|
||||
// gives us a valid (possibly zero) searchCount.
|
||||
if (this.props.searchInfo &&
|
||||
this.props.searchInfo.searchCount !== undefined &&
|
||||
this.props.searchInfo.searchCount !== null) {
|
||||
searchStatus = <div className="mx_RoomHeader_searchStatus">
|
||||
{ _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }
|
||||
</div>;
|
||||
}
|
||||
|
||||
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
|
||||
let settingsHint = false;
|
||||
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
|
||||
if (members) {
|
||||
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
|
||||
const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||
if (!nameEvent || !nameEvent.getContent().name) {
|
||||
settingsHint = true;
|
||||
}
|
||||
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
|
||||
let settingsHint = false;
|
||||
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
|
||||
if (members) {
|
||||
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
|
||||
const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||
if (!nameEvent || !nameEvent.getContent().name) {
|
||||
settingsHint = true;
|
||||
}
|
||||
}
|
||||
|
||||
let roomName = _t("Join Room");
|
||||
if (this.props.oobData && this.props.oobData.name) {
|
||||
roomName = this.props.oobData.name;
|
||||
} else if (this.props.room) {
|
||||
roomName = this.props.room.name;
|
||||
}
|
||||
|
||||
const emojiTextClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
|
||||
name =
|
||||
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
|
||||
<EmojiText dir="auto" element="div" className={emojiTextClasses} title={roomName}>{ roomName }</EmojiText>
|
||||
{ searchStatus }
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (canSetRoomTopic) {
|
||||
const RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor");
|
||||
topicElement = <RoomTopicEditor ref="topicEditor" room={this.props.room} />;
|
||||
} else {
|
||||
let topic;
|
||||
if (this.props.room) {
|
||||
const ev = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
||||
if (ev) {
|
||||
topic = ev.getContent().topic;
|
||||
}
|
||||
}
|
||||
topicElement =
|
||||
<div className="mx_RoomHeader_topic" ref="topic" title={topic} dir="auto">{ topic }</div>;
|
||||
let roomName = _t("Join Room");
|
||||
if (this.props.oobData && this.props.oobData.name) {
|
||||
roomName = this.props.oobData.name;
|
||||
} else if (this.props.room) {
|
||||
roomName = this.props.room.name;
|
||||
}
|
||||
|
||||
let roomAvatar = null;
|
||||
const emojiTextClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
|
||||
const name =
|
||||
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
|
||||
<EmojiText dir="auto" element="div" className={emojiTextClasses} title={roomName}>{ roomName }</EmojiText>
|
||||
{ searchStatus }
|
||||
</div>;
|
||||
|
||||
let topic;
|
||||
if (this.props.room) {
|
||||
const ev = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
||||
if (ev) {
|
||||
topic = ev.getContent().topic;
|
||||
}
|
||||
}
|
||||
const topicElement =
|
||||
<div className="mx_RoomHeader_topic" ref="topic" title={topic} dir="auto">{ topic }</div>;
|
||||
const avatarSize = 28;
|
||||
if (canSetRoomAvatar) {
|
||||
roomAvatar = (
|
||||
<div className="mx_RoomHeader_avatarPicker">
|
||||
<div onClick={this.onAvatarPickerClick}>
|
||||
<ChangeAvatar ref="changeAvatar" room={this.props.room} showUploadSection={false} width={avatarSize} height={avatarSize} />
|
||||
</div>
|
||||
<div className="mx_RoomHeader_avatarPicker_edit">
|
||||
<label htmlFor="avatarInput" ref="file_label">
|
||||
<img src={require("../../../../res/img/camera.svg")}
|
||||
alt={_t("Upload avatar")} title={_t("Upload avatar")}
|
||||
width="17" height="15" />
|
||||
</label>
|
||||
<input id="avatarInput" type="file" onChange={this.onAvatarSelected} />
|
||||
</div>
|
||||
<div className="mx_RoomHeader_avatarPicker_remove" onClick={this.onAvatarRemoveClick}>
|
||||
<img src={require("../../../../res/img/cancel.svg")}
|
||||
className="mx_filterFlipColor"
|
||||
width="10"
|
||||
alt={_t("Remove avatar")}
|
||||
title={_t("Remove avatar")} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.room || (this.props.oobData && this.props.oobData.name)) {
|
||||
roomAvatar = (
|
||||
<RoomAvatar room={this.props.room} width={avatarSize} height={avatarSize} oobData={this.props.oobData}
|
||||
viewAvatarOnClick={true} />
|
||||
);
|
||||
let roomAvatar;
|
||||
if (this.props.room) {
|
||||
roomAvatar = (<RoomAvatar
|
||||
room={this.props.room}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={true} />);
|
||||
}
|
||||
|
||||
if (this.props.onSettingsClick) {
|
||||
|
@ -389,7 +274,6 @@ module.exports = React.createClass({
|
|||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let rightRow;
|
||||
let manageIntegsButton;
|
||||
if (this.props.room && this.props.room.roomId && this.props.inRoom) {
|
||||
manageIntegsButton = <ManageIntegsButton
|
||||
|
@ -397,26 +281,23 @@ module.exports = React.createClass({
|
|||
/>;
|
||||
}
|
||||
|
||||
if (!this.props.editing) {
|
||||
rightRow =
|
||||
<div className="mx_RoomHeader_buttons">
|
||||
{ settingsButton }
|
||||
{ pinnedEventsButton }
|
||||
{ shareRoomButton }
|
||||
{ manageIntegsButton }
|
||||
{ forgetButton }
|
||||
{ searchButton }
|
||||
</div>;
|
||||
}
|
||||
const rightRow =
|
||||
<div className="mx_RoomHeader_buttons">
|
||||
{ settingsButton }
|
||||
{ pinnedEventsButton }
|
||||
{ shareRoomButton }
|
||||
{ manageIntegsButton }
|
||||
{ forgetButton }
|
||||
{ searchButton }
|
||||
</div>;
|
||||
|
||||
return (
|
||||
<div className={"mx_RoomHeader light-panel " + (this.props.editing ? "mx_RoomHeader_editing" : "")}>
|
||||
<div className="mx_RoomHeader light-panel">
|
||||
<div className="mx_RoomHeader_wrapper">
|
||||
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
|
||||
{ e2eIcon }
|
||||
{ name }
|
||||
{ topicElement }
|
||||
{ spinner }
|
||||
{ saveButton }
|
||||
{ cancelButton }
|
||||
{ rightRow }
|
||||
<RoomHeaderButtons collapsedRhs={this.props.collapsedRhs} />
|
||||
|
|
|
@ -32,11 +32,12 @@ import DMRoomMap from '../../../utils/DMRoomMap';
|
|||
const Receipt = require('../../../utils/Receipt');
|
||||
import TagOrderStore from '../../../stores/TagOrderStore';
|
||||
import RoomListStore from '../../../stores/RoomListStore';
|
||||
import CustomRoomTagStore from '../../../stores/CustomRoomTagStore';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import RoomSubList from '../../structures/RoomSubList';
|
||||
import ResizeHandle from '../elements/ResizeHandle';
|
||||
|
||||
import {Resizer} from '../../../resizer'
|
||||
import {Resizer} from '../../../resizer';
|
||||
import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2';
|
||||
const HIDE_CONFERENCE_CHANS = true;
|
||||
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
|
||||
|
@ -121,6 +122,7 @@ module.exports = React.createClass({
|
|||
incomingCall: null,
|
||||
selectedTags: [],
|
||||
hover: false,
|
||||
customTags: CustomRoomTagStore.getTags(),
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -170,6 +172,15 @@ module.exports = React.createClass({
|
|||
this._delayedRefreshRoomList();
|
||||
});
|
||||
|
||||
|
||||
if (SettingsStore.isFeatureEnabled("feature_custom_tags")) {
|
||||
this._customTagStoreToken = CustomRoomTagStore.addListener(() => {
|
||||
this.setState({
|
||||
customTags: CustomRoomTagStore.getTags(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.refreshRoomList();
|
||||
|
||||
// order of the sublists
|
||||
|
@ -183,7 +194,7 @@ module.exports = React.createClass({
|
|||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
const cfg = {
|
||||
layout: this._layout,
|
||||
getLayout: () => this._layout,
|
||||
};
|
||||
this.resizer = new Resizer(this.resizeContainer, Distributor, cfg);
|
||||
this.resizer.setClassNames({
|
||||
|
@ -266,6 +277,9 @@ module.exports = React.createClass({
|
|||
if (this._roomListStoreToken) {
|
||||
this._roomListStoreToken.remove();
|
||||
}
|
||||
if (this._customTagStoreToken) {
|
||||
this._customTagStoreToken.remove();
|
||||
}
|
||||
|
||||
// NB: GroupStore is not a Flux.Store
|
||||
if (this._groupStoreToken) {
|
||||
|
@ -717,7 +731,8 @@ module.exports = React.createClass({
|
|||
];
|
||||
const tagSubLists = Object.keys(this.state.lists)
|
||||
.filter((tagName) => {
|
||||
return !tagName.match(STANDARD_TAGS_REGEX);
|
||||
return (!this.state.customTags || this.state.customTags[tagName]) &&
|
||||
!tagName.match(STANDARD_TAGS_REGEX);
|
||||
}).map((tagName) => {
|
||||
return {
|
||||
list: this.state.lists[tagName],
|
||||
|
|
|
@ -38,7 +38,7 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
|||
this.state = {
|
||||
loading: true,
|
||||
error: null,
|
||||
unverifiedDevice: null,
|
||||
backupInfo: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -47,10 +47,12 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
|||
}
|
||||
|
||||
async _loadBackupStatus() {
|
||||
let backupSigStatus;
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
|
||||
this.setState({
|
||||
loading: false,
|
||||
backupInfo,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Unable to fetch key backup status", e);
|
||||
this.setState({
|
||||
|
@ -59,44 +61,20 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
|||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let unverifiedDevice;
|
||||
for (const sig of backupSigStatus.sigs) {
|
||||
if (sig.device && !sig.device.isVerified()) {
|
||||
unverifiedDevice = sig.device;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
loading: false,
|
||||
unverifiedDevice,
|
||||
});
|
||||
}
|
||||
|
||||
showSetupDialog = () => {
|
||||
if (this.state.unverifiedDevice) {
|
||||
if (this.state.backupInfo) {
|
||||
// A key backup exists for this account, but the creating device is not
|
||||
// verified, so we'll show the device verify dialog.
|
||||
// TODO: Should change to a restore key backup flow that checks the recovery
|
||||
// passphrase while at the same time also cross-signing the device as well in
|
||||
// a single flow (for cases where a key backup exists but the backup creating
|
||||
// device is unverified). Since we don't have that yet, we'll look for an
|
||||
// unverified device and verify it. Note that this means we won't restore
|
||||
// keys yet; instead we'll only trust the backup for sending our own new keys
|
||||
// to it.
|
||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
|
||||
userId: MatrixClientPeg.get().credentials.userId,
|
||||
device: this.state.unverifiedDevice,
|
||||
});
|
||||
return;
|
||||
// verified, so restore the backup which will give us the keys from it and
|
||||
// allow us to trust it (ie. upload keys to it)
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {});
|
||||
} else {
|
||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
|
||||
);
|
||||
}
|
||||
|
||||
// The default case assumes that a key backup doesn't exist for this account, so
|
||||
// we'll show the create key backup flow.
|
||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
|
||||
);
|
||||
}
|
||||
|
||||
onDontAskAgainClick = () => {
|
||||
|
@ -133,53 +111,40 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
|||
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
|
||||
|
||||
let body;
|
||||
let primaryCaption = _t("Set up");
|
||||
if (this.state.error) {
|
||||
body = <div className="error">
|
||||
{_t("Unable to load key backup status")}
|
||||
</div>;
|
||||
} else if (this.state.unverifiedDevice) {
|
||||
// A key backup exists for this account, but the creating device is not
|
||||
// verified.
|
||||
} else if (this.state.backupInfo) {
|
||||
// A key backup exists for this account, but we're not using it.
|
||||
body = <div>
|
||||
<p>{_t(
|
||||
"Secure Message Recovery has been set up on another device: <deviceName></deviceName>",
|
||||
{},
|
||||
{
|
||||
deviceName: () => <i>{this.state.unverifiedDevice.unsigned.device_display_name}</i>,
|
||||
},
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"To view your secure message history and ensure you can view new " +
|
||||
"messages on future devices, verify that device now.",
|
||||
"Secure Key Backup should be active on all of your devices to avoid " +
|
||||
"losing access to your encrypted messages.",
|
||||
)}</p>
|
||||
</div>;
|
||||
primaryCaption = _t("Verify device");
|
||||
} else {
|
||||
// The default case assumes that a key backup doesn't exist for this account.
|
||||
// (This component doesn't currently check that itself.)
|
||||
body = _t(
|
||||
"If you log out or use another device, you'll lose your " +
|
||||
"secure message history. To prevent this, set up Secure " +
|
||||
"Message Recovery.",
|
||||
"Securely back up your decryption keys to the server to make sure " +
|
||||
"you'll always be able to read your encrypted messages.",
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomRecoveryReminder">
|
||||
<div className="mx_RoomRecoveryReminder_header">{_t(
|
||||
"Secure Message Recovery",
|
||||
"Don't risk losing your encrypted messages!",
|
||||
)}</div>
|
||||
<div className="mx_RoomRecoveryReminder_body">{body}</div>
|
||||
<div className="mx_RoomRecoveryReminder_buttons">
|
||||
<AccessibleButton className="mx_RoomRecoveryReminder_button mx_RoomRecoveryReminder_secondary"
|
||||
onClick={this.onDontAskAgainClick}>
|
||||
{ _t("Don't ask again") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_RoomRecoveryReminder_button"
|
||||
onClick={this.onSetupClick}>
|
||||
{primaryCaption}
|
||||
{_t("Activate Secure Key Backup")}
|
||||
</AccessibleButton>
|
||||
<p><AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
|
||||
onClick={this.onDontAskAgainClick}>
|
||||
{ _t("No thanks, I'll download a copy of my decryption keys before I log out") }
|
||||
</AccessibleButton></p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,174 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
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 AddThreepid from '../../../AddThreepid';
|
||||
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
export default withMatrixClient(React.createClass({
|
||||
displayName: 'AddPhoneNumber',
|
||||
|
||||
propTypes: {
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
onThreepidAdded: PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
busy: false,
|
||||
phoneCountry: null,
|
||||
phoneNumber: "",
|
||||
msisdn_add_pending: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._addThreepid = null;
|
||||
this._addMsisdnInput = null;
|
||||
this._unmounted = false;
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
_onPhoneCountryChange: function(phoneCountry) {
|
||||
this.setState({ phoneCountry: phoneCountry.iso2 });
|
||||
},
|
||||
|
||||
_onPhoneNumberChange: function(ev) {
|
||||
this.setState({ phoneNumber: ev.target.value });
|
||||
},
|
||||
|
||||
_onAddMsisdnEditFinished: function(value, shouldSubmit) {
|
||||
if (!shouldSubmit) return;
|
||||
this._addMsisdn();
|
||||
},
|
||||
|
||||
_onAddMsisdnSubmit: function(ev) {
|
||||
ev.preventDefault();
|
||||
this._addMsisdn();
|
||||
},
|
||||
|
||||
_collectAddMsisdnInput: function(e) {
|
||||
this._addMsisdnInput = e;
|
||||
},
|
||||
|
||||
_addMsisdn: function() {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
this._addThreepid = new AddThreepid();
|
||||
// we always bind phone numbers when registering, so let's do the
|
||||
// same here.
|
||||
this._addThreepid.addMsisdn(this.state.phoneCountry, this.state.phoneNumber, true).then((resp) => {
|
||||
this._promptForMsisdnVerificationCode(resp.msisdn);
|
||||
}).catch((err) => {
|
||||
console.error("Unable to add phone number: " + err);
|
||||
const msg = err.message;
|
||||
Modal.createTrackedDialog('Add Phone Number Error', '', ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
description: msg,
|
||||
});
|
||||
}).finally(() => {
|
||||
if (this._unmounted) return;
|
||||
this.setState({msisdn_add_pending: false});
|
||||
}).done();
|
||||
this._addMsisdnInput.blur();
|
||||
this.setState({msisdn_add_pending: true});
|
||||
},
|
||||
|
||||
_promptForMsisdnVerificationCode: function(msisdn, err) {
|
||||
if (this._unmounted) return;
|
||||
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
||||
const msgElements = [
|
||||
<div key="_static" >{ _t("A text message has been sent to +%(msisdn)s. Please enter the verification code it contains", { msisdn: msisdn} ) }</div>,
|
||||
];
|
||||
if (err) {
|
||||
let msg = err.error;
|
||||
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
|
||||
msg = _t("Incorrect verification code");
|
||||
}
|
||||
msgElements.push(<div key="_error" className="error">{ msg }</div>);
|
||||
}
|
||||
Modal.createTrackedDialog('Prompt for MSISDN Verification Code', '', TextInputDialog, {
|
||||
title: _t("Enter Code"),
|
||||
description: <div>{ msgElements }</div>,
|
||||
button: _t("Submit"),
|
||||
onFinished: (should_verify, token) => {
|
||||
if (!should_verify) {
|
||||
this._addThreepid = null;
|
||||
return;
|
||||
}
|
||||
if (this._unmounted) return;
|
||||
this.setState({msisdn_add_pending: true});
|
||||
this._addThreepid.haveMsisdnToken(token).then(() => {
|
||||
this._addThreepid = null;
|
||||
this.setState({phoneNumber: ''});
|
||||
if (this.props.onThreepidAdded) this.props.onThreepidAdded();
|
||||
}).catch((err) => {
|
||||
this._promptForMsisdnVerificationCode(msisdn, err);
|
||||
}).finally(() => {
|
||||
if (this._unmounted) return;
|
||||
this.setState({msisdn_add_pending: false});
|
||||
}).done();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
if (this.state.msisdn_add_pending) {
|
||||
return <Loader />;
|
||||
} else if (this.props.matrixClient.isGuest()) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
|
||||
// XXX: This CSS relies on the CSS surrounding it in UserSettings as its in
|
||||
// a tabular format to align the submit buttons
|
||||
return (
|
||||
<form className="mx_UserSettings_profileTableRow" onSubmit={this._onAddMsisdnSubmit}>
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
<label>{ _t('Phone') }</label>
|
||||
</div>
|
||||
<div className="mx_UserSettings_profileInputCell">
|
||||
<div className="mx_UserSettings_phoneSection">
|
||||
<CountryDropdown onOptionChange={this._onPhoneCountryChange}
|
||||
className="mx_UserSettings_phoneCountry"
|
||||
value={this.state.phoneCountry}
|
||||
isSmall={true}
|
||||
/>
|
||||
<input type="text"
|
||||
ref={this._collectAddMsisdnInput}
|
||||
className="mx_UserSettings_phoneNumberField"
|
||||
placeholder={_t('Add phone number')}
|
||||
value={this.state.phoneNumber}
|
||||
onChange={this._onPhoneNumberChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||
<input type="image" value={_t("Add")} src={require("../../../../res/img/plus.svg")} width="14" height="14" />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
}));
|
|
@ -61,7 +61,7 @@ export default class DevicesPanel extends React.Component {
|
|||
let errtxt;
|
||||
if (error.httpStatus == 404) {
|
||||
// 404 probably means the HS doesn't yet support the API.
|
||||
errtxt = _t("Your home server does not support device management.");
|
||||
errtxt = _t("Your homeserver does not support device management.");
|
||||
} else {
|
||||
console.error("Error loading devices:", error);
|
||||
errtxt = _t("Unable to load device list");
|
||||
|
|
|
@ -27,7 +27,6 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
|
||||
this._startNewBackup = this._startNewBackup.bind(this);
|
||||
this._deleteBackup = this._deleteBackup.bind(this);
|
||||
this._verifyDevice = this._verifyDevice.bind(this);
|
||||
this._onKeyBackupSessionsRemaining =
|
||||
this._onKeyBackupSessionsRemaining.bind(this);
|
||||
this._onKeyBackupStatus = this._onKeyBackupStatus.bind(this);
|
||||
|
@ -133,19 +132,6 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
_verifyDevice(e) {
|
||||
const device = this.state.backupSigStatus.sigs[e.target.getAttribute('data-sigindex')].device;
|
||||
|
||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
|
||||
userId: MatrixClientPeg.get().credentials.userId,
|
||||
device: device,
|
||||
onFinished: () => {
|
||||
this._loadBackupStatus();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
@ -163,9 +149,8 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||
clientBackupStatus = _t("This device is using key backup");
|
||||
} else {
|
||||
// XXX: display why and how to fix it
|
||||
clientBackupStatus = _t(
|
||||
"This device is <b>not</b> using key backup", {},
|
||||
"This device is <b>not</b> using key backup. Restore the backup to start using it.", {},
|
||||
{b: x => <b>{x}</b>},
|
||||
);
|
||||
}
|
||||
|
@ -233,36 +218,37 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
let verifyButton;
|
||||
if (sig.device && !sig.device.isVerified()) {
|
||||
verifyButton = <div><br /><AccessibleButton className="mx_UserSettings_button"
|
||||
onClick={this._verifyDevice} data-sigindex={i}>
|
||||
{ _t("Verify...") }
|
||||
</AccessibleButton></div>;
|
||||
}
|
||||
|
||||
return <div key={i}>
|
||||
{sigStatus}
|
||||
{verifyButton}
|
||||
</div>;
|
||||
});
|
||||
if (this.state.backupSigStatus.sigs.length === 0) {
|
||||
backupSigStatuses = _t("Backup is not signed by any of your devices");
|
||||
}
|
||||
|
||||
let trustedLocally;
|
||||
if (this.state.backupSigStatus.trusted_locally) {
|
||||
trustedLocally = _t("This backup is trusted because it has been restored on this device");
|
||||
}
|
||||
|
||||
return <div>
|
||||
{_t("Backup version: ")}{this.state.backupInfo.version}<br />
|
||||
{_t("Algorithm: ")}{this.state.backupInfo.algorithm}<br />
|
||||
{clientBackupStatus}<br />
|
||||
{uploadStatus}
|
||||
<div>{backupSigStatuses}</div><br />
|
||||
<br />
|
||||
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
|
||||
{ _t("Restore backup") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="danger" onClick={this._deleteBackup}>
|
||||
{ _t("Delete backup") }
|
||||
</AccessibleButton>
|
||||
<div>{clientBackupStatus}</div>
|
||||
<details>
|
||||
<summary>{_t("Advanced")}</summary>
|
||||
<div>{_t("Backup version: ")}{this.state.backupInfo.version}</div>
|
||||
<div>{_t("Algorithm: ")}{this.state.backupInfo.algorithm}</div>
|
||||
{uploadStatus}
|
||||
<div>{backupSigStatuses}</div>
|
||||
<div>{trustedLocally}</div>
|
||||
</details>
|
||||
<p>
|
||||
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
|
||||
{ _t("Restore backup") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="danger" onClick={this._deleteBackup}>
|
||||
{ _t("Delete backup") }
|
||||
</AccessibleButton>
|
||||
</p>
|
||||
</div>;
|
||||
} else {
|
||||
return <div>
|
||||
|
|
|
@ -721,7 +721,7 @@ module.exports = React.createClass({
|
|||
<div>
|
||||
{masterPushRuleDiv}
|
||||
|
||||
<div className="mx_UserSettings_notifTable">
|
||||
<div className="mx_UserNotifSettings_notifTable">
|
||||
{ _t('All notifications are currently disabled for all targets.') }
|
||||
</div>
|
||||
</div>
|
||||
|
@ -775,7 +775,7 @@ module.exports = React.createClass({
|
|||
<td>{this.state.pushers[i].device_display_name}</td>
|
||||
</tr>);
|
||||
}
|
||||
devicesSection = (<table className="mx_UserSettings_devicesTable">
|
||||
devicesSection = (<table className="mx_UserNotifSettings_devicesTable">
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
|
@ -807,7 +807,7 @@ module.exports = React.createClass({
|
|||
|
||||
{masterPushRuleDiv}
|
||||
|
||||
<div className="mx_UserSettings_notifTable">
|
||||
<div className="mx_UserNotifSettings_notifTable">
|
||||
|
||||
{ spinner }
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import sdk from "../../../../index";
|
|||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import {MatrixClient} from "matrix-js-sdk";
|
||||
import dis from "../../../../dispatcher";
|
||||
import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch";
|
||||
|
||||
export default class GeneralRoomSettingsTab extends React.Component {
|
||||
static childContextTypes = {
|
||||
|
@ -33,12 +34,40 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
isRoomPublished: false, // loaded async
|
||||
};
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
MatrixClientPeg.get().getRoomDirectoryVisibility(this.props.roomId).then((result => {
|
||||
this.setState({isRoomPublished: result.visibility === 'public'});
|
||||
}));
|
||||
}
|
||||
|
||||
onRoomPublishChange = (e) => {
|
||||
const valueBefore = this.state.isRoomPublished;
|
||||
const newValue = !valueBefore;
|
||||
this.setState({isRoomPublished: newValue});
|
||||
|
||||
MatrixClientPeg.get().setRoomDirectoryVisibility(
|
||||
this.props.roomId,
|
||||
newValue ? 'public' : 'private',
|
||||
).catch(() => {
|
||||
// Roll back the local echo on the change
|
||||
this.setState({isRoomPublished: valueBefore});
|
||||
});
|
||||
};
|
||||
|
||||
_saveAliases = (e) => {
|
||||
// TODO: Live modification?
|
||||
if (!this.refs.aliasSettings) return;
|
||||
|
@ -67,6 +96,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
const room = client.getRoom(this.props.roomId);
|
||||
|
||||
const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this
|
||||
const canActuallySetAliases = room.currentState.mayClientSendStateEvent("m.room.aliases", client);
|
||||
const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
|
||||
const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", '');
|
||||
const aliasEvents = room.currentState.getStateEvents("m.room.aliases");
|
||||
|
@ -90,6 +120,14 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<LabelledToggleSwitch value={this.state.isRoomPublished}
|
||||
onChange={this.onRoomPublishChange}
|
||||
disabled={!canActuallySetAliases}
|
||||
label={_t("Publish this room to the public in %(domain)s's room directory?", {
|
||||
domain: client.getDomain(),
|
||||
})} />
|
||||
</div>
|
||||
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Flair")}</span>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
|
|
|
@ -16,11 +16,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import {_t} from "../../../../languageHandler";
|
||||
import MatrixClientPeg from "../../../../MatrixClientPeg";
|
||||
import GroupUserSettings from "../../groups/GroupUserSettings";
|
||||
import PropTypes from "prop-types";
|
||||
import {MatrixClient} from "matrix-js-sdk";
|
||||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
import ProfileSettings from "../ProfileSettings";
|
||||
import EmailAddresses from "../EmailAddresses";
|
||||
import PhoneNumbers from "../PhoneNumbers";
|
||||
|
@ -37,10 +32,6 @@ const Modal = require("../../../../Modal");
|
|||
const dis = require("../../../../dispatcher");
|
||||
|
||||
export default class GeneralUserSettingsTab extends React.Component {
|
||||
static childContextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
|
@ -50,12 +41,6 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
};
|
||||
}
|
||||
|
||||
_onLanguageChange = (newLanguage) => {
|
||||
if (this.state.language === newLanguage) return;
|
||||
|
||||
|
@ -105,16 +90,10 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
};
|
||||
|
||||
_renderProfileSection() {
|
||||
// HACK/TODO: Using DragDropContext feels wrong, but we need it.
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
|
||||
<ProfileSettings />
|
||||
|
||||
<span className="mx_SettingsTab_subheading">{_t("Flair")}</span>
|
||||
<DragDropContext>
|
||||
<GroupUserSettings />
|
||||
</DragDropContext>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,9 +18,7 @@ import React from 'react';
|
|||
import {_t} from "../../../../languageHandler";
|
||||
import PropTypes from "prop-types";
|
||||
import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore";
|
||||
import MatrixClientPeg from "../../../../MatrixClientPeg";
|
||||
import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch";
|
||||
const Modal = require("../../../../Modal");
|
||||
const sdk = require("../../../../index");
|
||||
|
||||
export class LabsSettingToggle extends React.Component {
|
||||
|
@ -28,38 +26,7 @@ export class LabsSettingToggle extends React.Component {
|
|||
featureId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
async _onLazyLoadChanging(enabling) {
|
||||
// don't prevent turning LL off when not supported
|
||||
if (enabling) {
|
||||
const supported = await MatrixClientPeg.get().doesServerSupportLazyLoading();
|
||||
if (!supported) {
|
||||
await new Promise((resolve) => {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: _t("Lazy loading members not supported"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("Lazy loading is not supported by your " +
|
||||
"current homeserver.") }
|
||||
</div>,
|
||||
button: _t("OK"),
|
||||
onFinished: resolve,
|
||||
});
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_onChange = async (checked) => {
|
||||
if (this.props.featureId === "feature_lazyloading") {
|
||||
const confirmed = await this._onLazyLoadChanging(checked);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await SettingsStore.setFeatureEnabled(this.props.featureId, checked);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
|
|
@ -113,6 +113,39 @@ export default class RolesRoomSettingsTab extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_onPowerLevelsChanged = (value, powerLevelKey) => {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
let plContent = room.currentState.getStateEvents('m.room.power_levels', '').getContent() || {};
|
||||
|
||||
// Clone the power levels just in case
|
||||
plContent = Object.assign({}, plContent);
|
||||
|
||||
const eventsLevelPrefix = "event_levels_";
|
||||
|
||||
value = parseInt(value);
|
||||
|
||||
if (powerLevelKey.startsWith(eventsLevelPrefix)) {
|
||||
// deep copy "events" object, Object.assign itself won't deep copy
|
||||
plContent["events"] = Object.assign({}, plContent["events"] || {});
|
||||
plContent["events"][powerLevelKey.slice(eventsLevelPrefix.length)] = value;
|
||||
} else {
|
||||
const keyPath = powerLevelKey.split('.');
|
||||
let parentObj;
|
||||
let currentObj = plContent;
|
||||
for (const key of keyPath) {
|
||||
if (!currentObj[key]) {
|
||||
currentObj[key] = {};
|
||||
}
|
||||
parentObj = currentObj;
|
||||
currentObj = currentObj[key];
|
||||
}
|
||||
parentObj[keyPath[keyPath.length - 1]] = value;
|
||||
}
|
||||
|
||||
client.sendStateEvent(this.props.roomId, "m.room.power_levels", plContent);
|
||||
};
|
||||
|
||||
render() {
|
||||
const PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
|
||||
|
@ -272,12 +305,12 @@ export default class RolesRoomSettingsTab extends React.Component {
|
|||
controlled={false}
|
||||
disabled={!canChangeLevels || currentUserLevel < value}
|
||||
powerLevelKey={key} // Will be sent as the second parameter to `onChange`
|
||||
onChange={this.onPowerLevelsChanged}
|
||||
onChange={this._onPowerLevelsChanged}
|
||||
/>
|
||||
</div>;
|
||||
});
|
||||
|
||||
const eventPowerSelectors = Object.keys(eventsLevels).map(function(eventType, i) {
|
||||
const eventPowerSelectors = Object.keys(eventsLevels).map((eventType, i) => {
|
||||
let label = plEventsToLabels[eventType];
|
||||
if (label) {
|
||||
label = _t(label);
|
||||
|
@ -296,7 +329,7 @@ export default class RolesRoomSettingsTab extends React.Component {
|
|||
controlled={false}
|
||||
disabled={!canChangeLevels || currentUserLevel < eventsLevels[eventType]}
|
||||
powerLevelKey={"event_levels_" + eventType}
|
||||
onChange={self.onPowerLevelsChanged}
|
||||
onChange={this._onPowerLevelsChanged}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -21,15 +21,33 @@ import MatrixClientPeg from "../../../../MatrixClientPeg";
|
|||
import sdk from "../../../../index";
|
||||
import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch";
|
||||
import {SettingLevel} from "../../../../settings/SettingsStore";
|
||||
import Modal from "../../../../Modal";
|
||||
|
||||
export default class SecurityRoomSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
joinRule: "invite",
|
||||
guestAccess: "can_join",
|
||||
history: "shared",
|
||||
encrypted: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount(): void {
|
||||
MatrixClientPeg.get().on("RoomState.events", this._onStateEvent);
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
const state = room.currentState;
|
||||
const joinRule = state.getStateEvents("m.room.join_rules", "").getContent()['join_rule'];
|
||||
const guestAccess = state.getStateEvents("m.room.guest_access", "").getContent()['guest_access'];
|
||||
const history = state.getStateEvents("m.room.history_visibility", "").getContent()['history_visibility'];
|
||||
const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
|
||||
this.setState({joinRule, guestAccess, history, encrypted});
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
|
@ -37,31 +55,24 @@ export default class SecurityRoomSettingsTab extends React.Component {
|
|||
}
|
||||
|
||||
_onStateEvent = (e) => {
|
||||
const refreshWhenTypes = ['m.room.join_rules', 'm.room.guest_access', 'm.room.history_visibility'];
|
||||
const refreshWhenTypes = [
|
||||
'm.room.join_rules',
|
||||
'm.room.guest_access',
|
||||
'm.room.history_visibility',
|
||||
'm.room.encryption',
|
||||
];
|
||||
if (refreshWhenTypes.includes(e.getType())) this.forceUpdate();
|
||||
};
|
||||
|
||||
_onEncryptionChange = (e) => {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('E2E Enable Warning', '', QuestionDialog, {
|
||||
title: _t('Warning!'),
|
||||
description: (
|
||||
<div>
|
||||
<p>{ _t('End-to-end encryption is in beta and may not be reliable') }.</p>
|
||||
<p>{ _t('You should not yet trust it to secure data') }.</p>
|
||||
<p>{ _t('Devices will not yet be able to decrypt history from before they joined the room') }.</p>
|
||||
<p>{ _t('Once encryption is enabled for a room it cannot be turned off again (for now)') }.</p>
|
||||
<p>{ _t('Encrypted messages will not be visible on clients that do not yet implement encryption') }.</p>
|
||||
</div>
|
||||
),
|
||||
onFinished: (confirm)=>{
|
||||
if (confirm) {
|
||||
return MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.roomId, "m.room.encryption",
|
||||
{ algorithm: "m.megolm.v1.aes-sha2" },
|
||||
);
|
||||
}
|
||||
},
|
||||
const beforeEncrypted = this.state.encrypted;
|
||||
this.setState({encrypted: true});
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.roomId, "m.room.encryption",
|
||||
{ algorithm: "m.megolm.v1.aes-sha2" },
|
||||
).catch((e) => {
|
||||
console.error(e);
|
||||
this.setState({encrypted: beforeEncrypted});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -69,9 +80,22 @@ export default class SecurityRoomSettingsTab extends React.Component {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const joinRule = "invite";
|
||||
const guestAccess = "can_join";
|
||||
|
||||
const beforeJoinRule = this.state.joinRule;
|
||||
const beforeGuestAccess = this.state.guestAccess;
|
||||
this.setState({joinRule, guestAccess});
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
client.sendStateEvent(this.props.roomId, "m.room.join_rules", {join_rule: "invite"}, "");
|
||||
client.sendStateEvent(this.props.roomId, "m.room.guest_access", {guest_access: "can_join"}, "");
|
||||
client.sendStateEvent(this.props.roomId, "m.room.join_rules", {join_rule: joinRule}, "").catch((e) => {
|
||||
console.error(e);
|
||||
this.setState({joinRule: beforeJoinRule});
|
||||
});
|
||||
client.sendStateEvent(this.props.roomId, "m.room.guest_access", {guest_access: guestAccess}, "").catch((e) => {
|
||||
console.error(e);
|
||||
this.setState({guestAccess: beforeGuestAccess});
|
||||
});
|
||||
};
|
||||
|
||||
_onRoomAccessRadioToggle = (ev) => {
|
||||
|
@ -105,24 +129,39 @@ export default class SecurityRoomSettingsTab extends React.Component {
|
|||
break;
|
||||
}
|
||||
|
||||
const beforeJoinRule = this.state.joinRule;
|
||||
const beforeGuestAccess = this.state.guestAccess;
|
||||
this.setState({joinRule, guestAccess});
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
client.sendStateEvent(this.props.roomId, "m.room.join_rules", {join_rule: joinRule}, "");
|
||||
client.sendStateEvent(this.props.roomId, "m.room.guest_access", {guest_access: guestAccess}, "");
|
||||
client.sendStateEvent(this.props.roomId, "m.room.join_rules", {join_rule: joinRule}, "").catch((e) => {
|
||||
console.error(e);
|
||||
this.setState({joinRule: beforeJoinRule});
|
||||
});
|
||||
client.sendStateEvent(this.props.roomId, "m.room.guest_access", {guest_access: guestAccess}, "").catch((e) => {
|
||||
console.error(e);
|
||||
this.setState({guestAccess: beforeGuestAccess});
|
||||
});
|
||||
};
|
||||
|
||||
_onHistoryRadioToggle = (ev) => {
|
||||
const beforeHistory = this.state.history;
|
||||
this.setState({history: ev.target.value});
|
||||
MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.history_visibility", {
|
||||
history_visibility: ev.target.value,
|
||||
}, "");
|
||||
}, "").catch((e) => {
|
||||
console.error(e);
|
||||
this.setState({history: beforeHistory});
|
||||
});
|
||||
};
|
||||
|
||||
_renderRoomAccess() {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
const joinRule = room.currentState.getStateEvents("m.room.join_rules", "").getContent()['join_rule'];
|
||||
const guestAccess = room.currentState.getStateEvents("m.room.guest_access", "").getContent()['guest_access'];
|
||||
const joinRule = this.state.joinRule;
|
||||
const guestAccess = this.state.guestAccess;
|
||||
const aliasEvents = room.currentState.getStateEvents("m.room.aliases") || [];
|
||||
const hasAliases = aliasEvents.includes((ev) => (ev.getContent().aliases || []).length);
|
||||
const hasAliases = !!aliasEvents.find((ev) => (ev.getContent().aliases || []).length > 0);
|
||||
|
||||
const canChangeAccess = room.currentState.mayClientSendStateEvent("m.room.join_rules", client)
|
||||
&& room.currentState.mayClientSendStateEvent("m.room.guest_access", client);
|
||||
|
@ -183,9 +222,8 @@ export default class SecurityRoomSettingsTab extends React.Component {
|
|||
|
||||
_renderHistory() {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
const state = room.currentState;
|
||||
const history = state.getStateEvents("m.room.history_visibility", "").getContent()['history_visibility'];
|
||||
const history = this.state.history;
|
||||
const state = client.getRoom(this.props.roomId).currentState;
|
||||
const canChangeHistory = state.mayClientSendStateEvent('m.room.history_visibility', client);
|
||||
|
||||
return (
|
||||
|
@ -231,7 +269,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
|
|||
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
const isEncrypted = client.isRoomEncrypted(this.props.roomId);
|
||||
const isEncrypted = this.state.encrypted;
|
||||
const hasEncryptionPermission = room.currentState.mayClientSendStateEvent("m.room.encryption", client);
|
||||
const canEnableEncryption = !isEncrypted && hasEncryptionPermission;
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../../languageHandler";
|
||||
import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore";
|
||||
import {SettingLevel} from "../../../../settings/SettingsStore";
|
||||
import MatrixClientPeg from "../../../../MatrixClientPeg";
|
||||
import * as FormattingUtils from "../../../../utils/FormattingUtils";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
|
@ -196,18 +196,15 @@ export default class SecuritySettingsTab extends React.Component {
|
|||
const DevicesPanel = sdk.getComponent('views.settings.DevicesPanel');
|
||||
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
|
||||
|
||||
let keyBackup = null;
|
||||
if (SettingsStore.isFeatureEnabled("feature_keybackup")) {
|
||||
const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel');
|
||||
keyBackup = (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Key backup")}</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
<KeyBackupPanel />
|
||||
</div>
|
||||
const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel');
|
||||
const keyBackup = (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Key backup")}</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
<KeyBackupPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_SecuritySettingsTab">
|
||||
|
|
|
@ -40,7 +40,13 @@ export default class VoiceSettingsTab extends React.Component {
|
|||
this._refreshMediaDevices();
|
||||
}
|
||||
|
||||
_refreshMediaDevices = async () => {
|
||||
_refreshMediaDevices = async (stream) => {
|
||||
if (stream) {
|
||||
// kill stream so that we don't leave it lingering around with webcam enabled etc
|
||||
// as here we called gUM to ask user for permission to their device names only
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
}
|
||||
|
||||
this.setState({
|
||||
mediaDevices: await CallMediaHandler.getDevices(),
|
||||
activeAudioOutput: CallMediaHandler.getAudioOutput(),
|
||||
|
|
|
@ -17,49 +17,145 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
|
||||
function capFirst(s) {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
export default class VerificationShowSas extends React.Component {
|
||||
static propTypes = {
|
||||
onDone: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
sas: PropTypes.string.isRequired,
|
||||
sas: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
sasVerified: false,
|
||||
};
|
||||
}
|
||||
|
||||
_onVerifiedStateChange = (newVal) => {
|
||||
this.setState({sasVerified: newVal});
|
||||
}
|
||||
|
||||
render() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const HexVerify = sdk.getComponent('views.elements.HexVerify');
|
||||
return <div>
|
||||
<p>{_t(
|
||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||
|
||||
let sasDisplay;
|
||||
let sasCaption;
|
||||
if (this.props.sas.emoji) {
|
||||
const emojiBlocks = this.props.sas.emoji.map(
|
||||
(emoji, i) => <div className="mx_VerificationShowSas_emojiSas_block" key={i}>
|
||||
<div className="mx_VerificationShowSas_emojiSas_emoji">
|
||||
<EmojiText addAlt={false}>{emoji[0]}</EmojiText>
|
||||
</div>
|
||||
<div className="mx_VerificationShowSas_emojiSas_label">
|
||||
{_t(capFirst(emoji[1]))}
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
sasDisplay = <div className="mx_VerificationShowSas_emojiSas">
|
||||
{emojiBlocks}
|
||||
</div>;
|
||||
sasCaption = _t(
|
||||
"Verify this user by confirming the following emoji appear on their screen.",
|
||||
);
|
||||
} else if (this.props.sas.decimal) {
|
||||
const numberBlocks = this.props.sas.decimal.map((num, i) => <span key={i}>
|
||||
{num}
|
||||
</span>);
|
||||
sasDisplay = <div className="mx_VerificationShowSas_decimalSas">
|
||||
{numberBlocks}
|
||||
</div>;
|
||||
sasCaption = _t(
|
||||
"Verify this user by confirming the following number appears on their screen.",
|
||||
)}</p>
|
||||
);
|
||||
} else {
|
||||
return <div>
|
||||
{_t("Unable to find a supported verification method.")}
|
||||
<DialogButtons
|
||||
primaryButton={_t('Cancel')}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this.props.onCancel}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div className="mx_VerificationShowSas">
|
||||
<p>{sasCaption}</p>
|
||||
<p>{_t(
|
||||
"For maximum security, we recommend you do this in person or use another " +
|
||||
"trusted means of communication.",
|
||||
)}</p>
|
||||
<HexVerify text={this.props.sas}
|
||||
onVerifiedStateChange={this._onVerifiedStateChange}
|
||||
/>
|
||||
<p>{_t(
|
||||
"To continue, click on each pair to confirm it's correct.",
|
||||
)}</p>
|
||||
{sasDisplay}
|
||||
<DialogButtons onPrimaryButtonClick={this.props.onDone}
|
||||
primaryButton={_t("Continue")}
|
||||
primaryDisabled={!this.state.sasVerified}
|
||||
hasCancel={true}
|
||||
onCancel={this.props.onCancel}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
// List of Emoji strings from the js-sdk, for i18n
|
||||
_td("Dog");
|
||||
_td("Cat");
|
||||
_td("Lion");
|
||||
_td("Horse");
|
||||
_td("Unicorn");
|
||||
_td("Pig");
|
||||
_td("Elephant");
|
||||
_td("Rabbit");
|
||||
_td("Panda");
|
||||
_td("Rooster");
|
||||
_td("Penguin");
|
||||
_td("Turtle");
|
||||
_td("Fish");
|
||||
_td("Octopus");
|
||||
_td("Butterfly");
|
||||
_td("Flower");
|
||||
_td("Tree");
|
||||
_td("Cactus");
|
||||
_td("Mushroom");
|
||||
_td("Globe");
|
||||
_td("Moon");
|
||||
_td("Cloud");
|
||||
_td("Fire");
|
||||
_td("Banana");
|
||||
_td("Apple");
|
||||
_td("Strawberry");
|
||||
_td("Corn");
|
||||
_td("Pizza");
|
||||
_td("Cake");
|
||||
_td("Heart");
|
||||
_td("Smiley");
|
||||
_td("Robot");
|
||||
_td("Hat");
|
||||
_td("Glasses");
|
||||
_td("Spanner");
|
||||
_td("Santa");
|
||||
_td("Thumbs up");
|
||||
_td("Umbrella");
|
||||
_td("Hourglass");
|
||||
_td("Clock");
|
||||
_td("Gift");
|
||||
_td("Light bulb");
|
||||
_td("Book");
|
||||
_td("Pencil");
|
||||
_td("Paperclip");
|
||||
_td("Scisors");
|
||||
_td("Padlock");
|
||||
_td("Key");
|
||||
_td("Hammer");
|
||||
_td("Telephone");
|
||||
_td("Flag");
|
||||
_td("Train");
|
||||
_td("Bicycle");
|
||||
_td("Aeroplane");
|
||||
_td("Rocket");
|
||||
_td("Trophy");
|
||||
_td("Ball");
|
||||
_td("Guitar");
|
||||
_td("Trumpet");
|
||||
_td("Bell");
|
||||
_td("Anchor");
|
||||
_td("Headphones");
|
||||
_td("Folder");
|
||||
_td("Pin");
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
"Permission Required": "Permission Required",
|
||||
"You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room",
|
||||
"The file '%(fileName)s' failed to upload": "The file '%(fileName)s' failed to upload",
|
||||
"The file '%(fileName)s' exceeds this home server's size limit for uploads": "The file '%(fileName)s' exceeds this home server's size limit for uploads",
|
||||
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "The file '%(fileName)s' exceeds this homeserver's size limit for uploads",
|
||||
"Upload Failed": "Upload Failed",
|
||||
"Failure to create room": "Failure to create room",
|
||||
"Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.",
|
||||
|
@ -133,7 +133,8 @@
|
|||
"Upgrades a room to a new version": "Upgrades a room to a new version",
|
||||
"Changes your display nickname": "Changes your display nickname",
|
||||
"Changes colour scheme of current room": "Changes colour scheme of current room",
|
||||
"Sets the room topic": "Sets the room topic",
|
||||
"Gets or sets the room topic": "Gets or sets the room topic",
|
||||
"This room has no topic.": "This room has no topic.",
|
||||
"Sets the room name": "Sets the room name",
|
||||
"Invites user with given id to current room": "Invites user with given id to current room",
|
||||
"Joins room with given alias": "Joins room with given alias",
|
||||
|
@ -185,6 +186,12 @@
|
|||
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s removed the room name.",
|
||||
"%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s changed the room name to %(roomName)s.",
|
||||
"%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s upgraded this room.",
|
||||
"%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s made the room public to whoever knows the link.",
|
||||
"%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s made the room invite only.",
|
||||
"%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s changed the join rule to %(rule)s",
|
||||
"%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s has allowed guests to join the room.",
|
||||
"%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s has prevented guests from joining the room.",
|
||||
"%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s changed guest access to %(rule)s",
|
||||
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.",
|
||||
"%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s added %(addedAddresses)s as addresses for this room.",
|
||||
"%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s added %(addedAddresses)s as an address for this room.",
|
||||
|
@ -262,12 +269,9 @@
|
|||
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
|
||||
"Failed to join room": "Failed to join room",
|
||||
"Message Pinning": "Message Pinning",
|
||||
"Tabbed settings": "Tabbed settings",
|
||||
"Custom user status messages": "Custom user status messages",
|
||||
"Increase performance by only loading room members on first view": "Increase performance by only loading room members on first view",
|
||||
"Backup of encryption keys to server": "Backup of encryption keys to server",
|
||||
"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",
|
||||
"Two-way device verification using short text": "Two-way device verification using short text",
|
||||
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
|
||||
"Use compact timeline layout": "Use compact timeline layout",
|
||||
"Show a placeholder for removed messages": "Show a placeholder for removed messages",
|
||||
|
@ -327,17 +331,75 @@
|
|||
"You've successfully verified this user.": "You've successfully verified this user.",
|
||||
"Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.",
|
||||
"Got It": "Got It",
|
||||
"Verify this user by confirming the following emoji appear on their screen.": "Verify this user by confirming the following emoji appear on their screen.",
|
||||
"Verify this user by confirming the following number appears on their screen.": "Verify this user by confirming the following number appears on their screen.",
|
||||
"Unable to find a supported verification method.": "Unable to find a supported verification method.",
|
||||
"For maximum security, we recommend you do this in person or use another trusted means of communication.": "For maximum security, we recommend you do this in person or use another trusted means of communication.",
|
||||
"To continue, click on each pair to confirm it's correct.": "To continue, click on each pair to confirm it's correct.",
|
||||
"Continue": "Continue",
|
||||
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains",
|
||||
"Incorrect verification code": "Incorrect verification code",
|
||||
"Enter Code": "Enter Code",
|
||||
"Submit": "Submit",
|
||||
"Phone": "Phone",
|
||||
"Add phone number": "Add phone number",
|
||||
"Add": "Add",
|
||||
"Dog": "Dog",
|
||||
"Cat": "Cat",
|
||||
"Lion": "Lion",
|
||||
"Horse": "Horse",
|
||||
"Unicorn": "Unicorn",
|
||||
"Pig": "Pig",
|
||||
"Elephant": "Elephant",
|
||||
"Rabbit": "Rabbit",
|
||||
"Panda": "Panda",
|
||||
"Rooster": "Rooster",
|
||||
"Penguin": "Penguin",
|
||||
"Turtle": "Turtle",
|
||||
"Fish": "Fish",
|
||||
"Octopus": "Octopus",
|
||||
"Butterfly": "Butterfly",
|
||||
"Flower": "Flower",
|
||||
"Tree": "Tree",
|
||||
"Cactus": "Cactus",
|
||||
"Mushroom": "Mushroom",
|
||||
"Globe": "Globe",
|
||||
"Moon": "Moon",
|
||||
"Cloud": "Cloud",
|
||||
"Fire": "Fire",
|
||||
"Banana": "Banana",
|
||||
"Apple": "Apple",
|
||||
"Strawberry": "Strawberry",
|
||||
"Corn": "Corn",
|
||||
"Pizza": "Pizza",
|
||||
"Cake": "Cake",
|
||||
"Heart": "Heart",
|
||||
"Smiley": "Smiley",
|
||||
"Robot": "Robot",
|
||||
"Hat": "Hat",
|
||||
"Glasses": "Glasses",
|
||||
"Spanner": "Spanner",
|
||||
"Santa": "Santa",
|
||||
"Thumbs up": "Thumbs up",
|
||||
"Umbrella": "Umbrella",
|
||||
"Hourglass": "Hourglass",
|
||||
"Clock": "Clock",
|
||||
"Gift": "Gift",
|
||||
"Light bulb": "Light bulb",
|
||||
"Book": "Book",
|
||||
"Pencil": "Pencil",
|
||||
"Paperclip": "Paperclip",
|
||||
"Scisors": "Scisors",
|
||||
"Padlock": "Padlock",
|
||||
"Key": "Key",
|
||||
"Hammer": "Hammer",
|
||||
"Telephone": "Telephone",
|
||||
"Flag": "Flag",
|
||||
"Train": "Train",
|
||||
"Bicycle": "Bicycle",
|
||||
"Aeroplane": "Aeroplane",
|
||||
"Rocket": "Rocket",
|
||||
"Trophy": "Trophy",
|
||||
"Ball": "Ball",
|
||||
"Guitar": "Guitar",
|
||||
"Trumpet": "Trumpet",
|
||||
"Bell": "Bell",
|
||||
"Anchor": "Anchor",
|
||||
"Headphones": "Headphones",
|
||||
"Folder": "Folder",
|
||||
"Pin": "Pin",
|
||||
"Failed to upload profile picture!": "Failed to upload profile picture!",
|
||||
"Upload new:": "Upload new:",
|
||||
"No display name": "No display name",
|
||||
|
@ -352,7 +414,7 @@
|
|||
"New Password": "New Password",
|
||||
"Confirm password": "Confirm password",
|
||||
"Change Password": "Change Password",
|
||||
"Your home server does not support device management.": "Your home server does not support device management.",
|
||||
"Your homeserver does not support device management.": "Your homeserver does not support device management.",
|
||||
"Unable to load device list": "Unable to load device list",
|
||||
"Authentication": "Authentication",
|
||||
"Delete %(count)s devices|other": "Delete %(count)s devices",
|
||||
|
@ -371,6 +433,7 @@
|
|||
"This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address",
|
||||
"Unable to add email address": "Unable to add email address",
|
||||
"Unable to verify email address.": "Unable to verify email address.",
|
||||
"Add": "Add",
|
||||
"We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.",
|
||||
"Email Address": "Email Address",
|
||||
"Disable Notifications": "Disable Notifications",
|
||||
|
@ -380,7 +443,7 @@
|
|||
"Delete backup": "Delete backup",
|
||||
"Unable to load key backup status": "Unable to load key backup status",
|
||||
"This device is using key backup": "This device is using key backup",
|
||||
"This device is <b>not</b> using key backup": "This device is <b>not</b> using key backup",
|
||||
"This device is <b>not</b> using key backup. Restore the backup to start using it.": "This device is <b>not</b> using key backup. Restore the backup to start using it.",
|
||||
"Backing up %(sessionsRemaining)s keys...": "Backing up %(sessionsRemaining)s keys...",
|
||||
"All keys backed up": "All keys backed up",
|
||||
"Backup has a signature from <verify>unknown</verify> device with ID %(deviceId)s.": "Backup has a signature from <verify>unknown</verify> device with ID %(deviceId)s.",
|
||||
|
@ -389,8 +452,9 @@
|
|||
"Backup has a <validity>valid</validity> signature from <verify>unverified</verify> device <device></device>": "Backup has a <validity>valid</validity> signature from <verify>unverified</verify> device <device></device>",
|
||||
"Backup has an <validity>invalid</validity> signature from <verify>verified</verify> device <device></device>": "Backup has an <validity>invalid</validity> signature from <verify>verified</verify> device <device></device>",
|
||||
"Backup has an <validity>invalid</validity> signature from <verify>unverified</verify> device <device></device>": "Backup has an <validity>invalid</validity> signature from <verify>unverified</verify> device <device></device>",
|
||||
"Verify...": "Verify...",
|
||||
"Backup is not signed by any of your devices": "Backup is not signed by any of your devices",
|
||||
"This backup is trusted because it has been restored on this device": "This backup is trusted because it has been restored on this device",
|
||||
"Advanced": "Advanced",
|
||||
"Backup version: ": "Backup version: ",
|
||||
"Algorithm: ": "Algorithm: ",
|
||||
"Restore backup": "Restore backup",
|
||||
|
@ -424,6 +488,8 @@
|
|||
"On": "On",
|
||||
"Noisy": "Noisy",
|
||||
"Unable to verify phone number.": "Unable to verify phone number.",
|
||||
"Incorrect verification code": "Incorrect verification code",
|
||||
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains",
|
||||
"Verification code": "Verification code",
|
||||
"Phone Number": "Phone Number",
|
||||
"Profile picture": "Profile picture",
|
||||
|
@ -432,7 +498,6 @@
|
|||
"Save": "Save",
|
||||
"This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers",
|
||||
"Upgrade room to version %(ver)s": "Upgrade room to version %(ver)s",
|
||||
"Advanced": "Advanced",
|
||||
"Room information": "Room information",
|
||||
"Internal room ID:": "Internal room ID:",
|
||||
"Room version": "Room version",
|
||||
|
@ -442,6 +507,7 @@
|
|||
"Flair": "Flair",
|
||||
"General": "General",
|
||||
"Room Addresses": "Room Addresses",
|
||||
"Publish this room to the public in %(domain)s's room directory?": "Publish this room to the public in %(domain)s's room directory?",
|
||||
"URL Previews": "URL Previews",
|
||||
"Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
|
||||
"Success": "Success",
|
||||
|
@ -479,8 +545,6 @@
|
|||
"Identity Server is": "Identity Server is",
|
||||
"Access Token:": "Access Token:",
|
||||
"click to reveal": "click to reveal",
|
||||
"Lazy loading members not supported": "Lazy loading members not supported",
|
||||
"Lazy loading is not supported by your current homeserver.": "Lazy loading is not supported by your current homeserver.",
|
||||
"Labs": "Labs",
|
||||
"Notifications": "Notifications",
|
||||
"Start automatically after system login": "Start automatically after system login",
|
||||
|
@ -515,11 +579,6 @@
|
|||
"To send events of type <eventType/>, you must be a": "To send events of type <eventType/>, you must be a",
|
||||
"Roles & Permissions": "Roles & Permissions",
|
||||
"Permissions": "Permissions",
|
||||
"End-to-end encryption is in beta and may not be reliable": "End-to-end encryption is in beta and may not be reliable",
|
||||
"You should not yet trust it to secure data": "You should not yet trust it to secure data",
|
||||
"Devices will not yet be able to decrypt history from before they joined the room": "Devices will not yet be able to decrypt history from before they joined the room",
|
||||
"Once encryption is enabled for a room it cannot be turned off again (for now)": "Once encryption is enabled for a room it cannot be turned off again (for now)",
|
||||
"Encrypted messages will not be visible on clients that do not yet implement encryption": "Encrypted messages will not be visible on clients that do not yet implement encryption",
|
||||
"Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.",
|
||||
"Click here to fix": "Click here to fix",
|
||||
"To link to this room, please add an alias.": "To link to this room, please add an alias.",
|
||||
|
@ -571,6 +630,10 @@
|
|||
" (unsupported)": " (unsupported)",
|
||||
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.": "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
|
||||
"Ongoing conference call%(supportedText)s.": "Ongoing conference call%(supportedText)s.",
|
||||
"Some devices for this user are not trusted": "Some devices for this user are not trusted",
|
||||
"Some devices in this encrypted room are not trusted": "Some devices in this encrypted room are not trusted",
|
||||
"All devices for this user are trusted": "All devices for this user are trusted",
|
||||
"All devices in this encrypted room are trusted": "All devices in this encrypted room are trusted",
|
||||
"This event could not be displayed": "This event could not be displayed",
|
||||
"%(senderName)s sent an image": "%(senderName)s sent an image",
|
||||
"%(senderName)s sent a video": "%(senderName)s sent a video",
|
||||
|
@ -582,16 +645,10 @@
|
|||
"Key request sent.": "Key request sent.",
|
||||
"<requestLink>Re-request encryption keys</requestLink> from your other devices.": "<requestLink>Re-request encryption keys</requestLink> from your other devices.",
|
||||
"Undecryptable": "Undecryptable",
|
||||
"Encrypting": "Encrypting",
|
||||
"Encrypted, not sent": "Encrypted, not sent",
|
||||
"Encrypted by a verified device": "Encrypted by a verified device",
|
||||
"Encrypted by an unverified device": "Encrypted by an unverified device",
|
||||
"Unencrypted message": "Unencrypted message",
|
||||
"Please select the destination room for this message": "Please select the destination room for this message",
|
||||
"Scroll to bottom of page": "Scroll to bottom of page",
|
||||
"Blacklisted": "Blacklisted",
|
||||
"Verified": "Verified",
|
||||
"Unverified": "Unverified",
|
||||
"device id: ": "device id: ",
|
||||
"Disinvite": "Disinvite",
|
||||
"Kick": "Kick",
|
||||
|
@ -643,8 +700,6 @@
|
|||
"Are you sure you want to upload the following files?": "Are you sure you want to upload the following files?",
|
||||
"The following files cannot be uploaded:": "The following files cannot be uploaded:",
|
||||
"Upload Files": "Upload Files",
|
||||
"Encrypted room": "Encrypted room",
|
||||
"Unencrypted room": "Unencrypted room",
|
||||
"Hangup": "Hangup",
|
||||
"Voice call": "Voice call",
|
||||
"Video call": "Video call",
|
||||
|
@ -691,12 +746,9 @@
|
|||
"Unnamed room": "Unnamed room",
|
||||
"World readable": "World readable",
|
||||
"Guests can join": "Guests can join",
|
||||
"Failed to set avatar.": "Failed to set avatar.",
|
||||
"(~%(count)s results)|other": "(~%(count)s results)",
|
||||
"(~%(count)s results)|one": "(~%(count)s result)",
|
||||
"Join Room": "Join Room",
|
||||
"Upload avatar": "Upload avatar",
|
||||
"Remove avatar": "Remove avatar",
|
||||
"Settings": "Settings",
|
||||
"Forget room": "Forget room",
|
||||
"Search": "Search",
|
||||
|
@ -733,28 +785,11 @@
|
|||
"You are trying to access a room.": "You are trying to access a room.",
|
||||
"<a>Click here</a> to join the discussion!": "<a>Click here</a> to join the discussion!",
|
||||
"This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled",
|
||||
"Set up": "Set up",
|
||||
"Secure Message Recovery has been set up on another device: <deviceName></deviceName>": "Secure Message Recovery has been set up on another device: <deviceName></deviceName>",
|
||||
"To view your secure message history and ensure you can view new messages on future devices, verify that device now.": "To view your secure message history and ensure you can view new messages on future devices, verify that device now.",
|
||||
"Verify device": "Verify device",
|
||||
"If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.": "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.",
|
||||
"Secure Message Recovery": "Secure Message Recovery",
|
||||
"Don't ask again": "Don't ask again",
|
||||
"Privacy warning": "Privacy warning",
|
||||
"Changes to who can read history will only apply to future messages in this room": "Changes to who can read history will only apply to future messages in this room",
|
||||
"The visibility of existing history will be unchanged": "The visibility of existing history will be unchanged",
|
||||
"unknown error code": "unknown error code",
|
||||
"Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s",
|
||||
"Enable encryption": "Enable encryption",
|
||||
"(warning: cannot be disabled again!)": "(warning: cannot be disabled again!)",
|
||||
"Encryption is enabled in this room": "Encryption is enabled in this room",
|
||||
"Encryption is not enabled in this room": "Encryption is not enabled in this room",
|
||||
"Favourite": "Favourite",
|
||||
"Tagged as: ": "Tagged as: ",
|
||||
"To link to a room it must have <a>an address</a>.": "To link to a room it must have <a>an address</a>.",
|
||||
"Publish this room to the public in %(domain)s's room directory?": "Publish this room to the public in %(domain)s's room directory?",
|
||||
"Internal room ID: ": "Internal room ID: ",
|
||||
"Room version number: ": "Room version number: ",
|
||||
"Secure Key Backup should be active on all of your devices to avoid losing access to your encrypted messages.": "Secure Key Backup should be active on all of your devices to avoid losing access to your encrypted messages.",
|
||||
"Securely back up your decryption keys to the server to make sure you'll always be able to read your encrypted messages.": "Securely back up your decryption keys to the server to make sure you'll always be able to read your encrypted messages.",
|
||||
"Don't risk losing your encrypted messages!": "Don't risk losing your encrypted messages!",
|
||||
"Activate Secure Key Backup": "Activate Secure Key Backup",
|
||||
"No thanks, I'll download a copy of my decryption keys before I log out": "No thanks, I'll download a copy of my decryption keys before I log out",
|
||||
"Add a topic": "Add a topic",
|
||||
"This room is using an unstable room version. If you aren't expecting this, please upgrade the room.": "This room is using an unstable room version. If you aren't expecting this, please upgrade the room.",
|
||||
"Click here to upgrade to the latest room version.": "Click here to upgrade to the latest room version.",
|
||||
|
@ -879,16 +914,17 @@
|
|||
"Delete widget": "Delete widget",
|
||||
"Failed to remove widget": "Failed to remove widget",
|
||||
"An error ocurred whilst trying to remove the widget from the room": "An error ocurred whilst trying to remove the widget from the room",
|
||||
"Revoke widget access": "Revoke widget access",
|
||||
"Minimize apps": "Minimize apps",
|
||||
"Reload widget": "Reload widget",
|
||||
"Popout widget": "Popout widget",
|
||||
"Picture": "Picture",
|
||||
"Edit": "Edit",
|
||||
"Revoke widget access": "Revoke widget access",
|
||||
"Create new room": "Create new room",
|
||||
"Unblacklist": "Unblacklist",
|
||||
"Blacklist": "Blacklist",
|
||||
"Unverify": "Unverify",
|
||||
"Verify...": "Verify...",
|
||||
"Join": "Join",
|
||||
"No results": "No results",
|
||||
"Delete": "Delete",
|
||||
|
@ -1020,6 +1056,7 @@
|
|||
"Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)",
|
||||
"To continue, please enter your password:": "To continue, please enter your password:",
|
||||
"password": "password",
|
||||
"Verify device": "Verify device",
|
||||
"Use Legacy Verification (for older clients)": "Use Legacy Verification (for older clients)",
|
||||
"Verify by comparing a short text string.": "Verify by comparing a short text string.",
|
||||
"Begin Verifying": "Begin Verifying",
|
||||
|
@ -1065,7 +1102,6 @@
|
|||
"Updating Riot": "Updating Riot",
|
||||
"When you log out, you'll lose your secure message history. To prevent this, set up a recovery method.": "When you log out, you'll lose your secure message history. To prevent this, set up a recovery method.",
|
||||
"Alternatively, advanced users can also manually export encryption keys in <a>Settings</a> before logging out.": "Alternatively, advanced users can also manually export encryption keys in <a>Settings</a> before logging out.",
|
||||
"For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.",
|
||||
"Set a Recovery Method": "Set a Recovery Method",
|
||||
"I understand, log out without": "I understand, log out without",
|
||||
"When signing in again, you can access encrypted chat history by restoring your key backup. You'll need your recovery passphrase or, if you didn't set a recovery passphrase, your recovery key (that you downloaded).": "When signing in again, you can access encrypted chat history by restoring your key backup. You'll need your recovery passphrase or, if you didn't set a recovery passphrase, your recovery key (that you downloaded).",
|
||||
|
@ -1073,7 +1109,7 @@
|
|||
"To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.": "To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.",
|
||||
"Report bugs & give feedback": "Report bugs & give feedback",
|
||||
"Go back": "Go back",
|
||||
"Visit old settings": "Visit old settings",
|
||||
"Room Settings": "Room Settings",
|
||||
"Failed to upgrade room": "Failed to upgrade room",
|
||||
"The room upgrade could not be completed": "The room upgrade could not be completed",
|
||||
"Upgrade this room to version %(version)s": "Upgrade this room to version %(version)s",
|
||||
|
@ -1125,10 +1161,12 @@
|
|||
"\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.",
|
||||
"Unknown devices": "Unknown devices",
|
||||
"Unable to load backup status": "Unable to load backup status",
|
||||
"Recovery Key Mismatch": "Recovery Key Mismatch",
|
||||
"Backup could not be decrypted with this key: please verify that you entered the correct recovery key.": "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.",
|
||||
"Incorrect Recovery Passphrase": "Incorrect Recovery Passphrase",
|
||||
"Backup could not be decrypted with this passphrase: please verify that you entered the correct recovery passphrase.": "Backup could not be decrypted with this passphrase: please verify that you entered the correct recovery passphrase.",
|
||||
"Unable to restore backup": "Unable to restore backup",
|
||||
"No backup found!": "No backup found!",
|
||||
"Error Restoring Backup": "Error Restoring Backup",
|
||||
"Backup could not be decrypted with this key: please verify that you entered the correct recovery key.": "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.",
|
||||
"Backup Restored": "Backup Restored",
|
||||
"Failed to decrypt %(failedCount)s sessions!": "Failed to decrypt %(failedCount)s sessions!",
|
||||
"Restored %(sessionCount)s session keys": "Restored %(sessionCount)s session keys",
|
||||
|
@ -1163,11 +1201,14 @@
|
|||
"Source URL": "Source URL",
|
||||
"Collapse Reply Thread": "Collapse Reply Thread",
|
||||
"Failed to set Direct Message status of room": "Failed to set Direct Message status of room",
|
||||
"unknown error code": "unknown error code",
|
||||
"Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s",
|
||||
"All messages (noisy)": "All messages (noisy)",
|
||||
"All messages": "All messages",
|
||||
"Mentions only": "Mentions only",
|
||||
"Leave": "Leave",
|
||||
"Forget": "Forget",
|
||||
"Favourite": "Favourite",
|
||||
"Low Priority": "Low Priority",
|
||||
"Direct Chat": "Direct Chat",
|
||||
"Clear status": "Clear status",
|
||||
|
@ -1178,7 +1219,7 @@
|
|||
"Login": "Login",
|
||||
"powered by Matrix": "powered by Matrix",
|
||||
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>": "Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
|
||||
"This Home Server would like to make sure you are not a robot": "This Home Server would like to make sure you are not a robot",
|
||||
"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",
|
||||
"You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use this app with an existing Matrix account on a different homeserver.": "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use this app with an existing Matrix account on a different homeserver.",
|
||||
"You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.",
|
||||
|
@ -1192,6 +1233,7 @@
|
|||
"A text message has been sent to %(msisdn)s": "A text message has been sent to %(msisdn)s",
|
||||
"Please enter the code it contains:": "Please enter the code it contains:",
|
||||
"Code": "Code",
|
||||
"Submit": "Submit",
|
||||
"Start authentication": "Start authentication",
|
||||
"Your Modular server": "Your Modular server",
|
||||
"Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.": "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.",
|
||||
|
@ -1204,10 +1246,11 @@
|
|||
"Username": "Username",
|
||||
"Mobile phone number": "Mobile phone number",
|
||||
"Not sure of your password? <a>Set a new one</a>": "Not sure of your password? <a>Set a new one</a>",
|
||||
"Your account": "Your account",
|
||||
"Your %(serverName)s account": "Your %(serverName)s account",
|
||||
"Sign in with": "Sign in with",
|
||||
"Sign in": "Sign in",
|
||||
"Sign in to %(serverName)s": "Sign in to %(serverName)s",
|
||||
"Change": "Change",
|
||||
"Sign in with": "Sign in with",
|
||||
"Phone": "Phone",
|
||||
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
|
||||
"Create your account": "Create your account",
|
||||
"Create your %(serverName)s account": "Create your %(serverName)s account",
|
||||
|
@ -1215,7 +1258,7 @@
|
|||
"Email (optional)": "Email (optional)",
|
||||
"Phone (optional)": "Phone (optional)",
|
||||
"Confirm": "Confirm",
|
||||
"Use an email address to receover your account. Other users can invite you to rooms using your contact details.": "Use an email address to receover your account. Other users can invite you to rooms using your contact details.",
|
||||
"Use an email address to recover your account. Other users can invite you to rooms using your contact details.": "Use an email address to recover your account. Other users can invite you to rooms using your contact details.",
|
||||
"Other servers": "Other servers",
|
||||
"Enter custom server URLs <a>What does this mean?</a>": "Enter custom server URLs <a>What does this mean?</a>",
|
||||
"Homeserver URL": "Homeserver URL",
|
||||
|
@ -1232,6 +1275,7 @@
|
|||
"<safariLink>Safari</safariLink> and <operaLink>Opera</operaLink> work too.": "<safariLink>Safari</safariLink> and <operaLink>Opera</operaLink> work too.",
|
||||
"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!": "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!",
|
||||
"I understand the risks and wish to continue": "I understand the risks and wish to continue",
|
||||
"Couldn't load page": "Couldn't load page",
|
||||
"You must <a>register</a> to use this functionality": "You must <a>register</a> to use this functionality",
|
||||
"You must join the room to see its files": "You must join the room to see its files",
|
||||
"There are no visible files in this room": "There are no visible files in this room",
|
||||
|
@ -1271,13 +1315,11 @@
|
|||
"Everyone": "Everyone",
|
||||
"Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!": "Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!",
|
||||
"Long Description (HTML)": "Long Description (HTML)",
|
||||
"Upload avatar": "Upload avatar",
|
||||
"Description": "Description",
|
||||
"Community %(groupId)s not found": "Community %(groupId)s not found",
|
||||
"This Home server does not support communities": "This Home server does not support communities",
|
||||
"This homeserver does not support communities": "This homeserver does not support communities",
|
||||
"Failed to load %(groupId)s": "Failed to load %(groupId)s",
|
||||
"Couldn't load home page": "Couldn't load home page",
|
||||
"You are currently using Riot anonymously as a guest.": "You are currently using Riot anonymously as a guest.",
|
||||
"If you would like to create a Matrix account you can <a>register</a> now.": "If you would like to create a Matrix account you can <a>register</a> now.",
|
||||
"Invalid configuration: Cannot supply a default homeserver URL and a default server name": "Invalid configuration: Cannot supply a default homeserver URL and a default server name",
|
||||
"Failed to reject invitation": "Failed to reject invitation",
|
||||
"This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.",
|
||||
|
@ -1301,8 +1343,8 @@
|
|||
"Create a new community": "Create a new community",
|
||||
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
|
||||
"You have no visible notifications": "You have no visible notifications",
|
||||
"Failed to get protocol list from Home Server": "Failed to get protocol list from Home Server",
|
||||
"The Home Server may be too old to support third party networks": "The Home Server may be too old to support third party networks",
|
||||
"Failed to get protocol list from homeserver": "Failed to get protocol list from homeserver",
|
||||
"The homeserver may be too old to support third party networks": "The homeserver may be too old to support third party networks",
|
||||
"Failed to get public room list": "Failed to get public room list",
|
||||
"The server may be unavailable or overloaded": "The server may be unavailable or overloaded",
|
||||
"Delete the room alias %(alias)s and remove %(name)s from the directory?": "Delete the room alias %(alias)s and remove %(name)s from the directory?",
|
||||
|
@ -1341,7 +1383,6 @@
|
|||
"No more results": "No more results",
|
||||
"Unknown room %(roomId)s": "Unknown room %(roomId)s",
|
||||
"Room": "Room",
|
||||
"Failed to save settings": "Failed to save settings",
|
||||
"Failed to reject invite": "Failed to reject invite",
|
||||
"Fill screen": "Fill screen",
|
||||
"Click to unmute video": "Click to unmute video",
|
||||
|
@ -1357,53 +1398,31 @@
|
|||
"Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others",
|
||||
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
|
||||
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
|
||||
"Can't load user settings": "Can't load user settings",
|
||||
"Server may be unavailable or overloaded": "Server may be unavailable or overloaded",
|
||||
"Remove Contact Information?": "Remove Contact Information?",
|
||||
"Remove %(threePid)s?": "Remove %(threePid)s?",
|
||||
"Interface Language": "Interface Language",
|
||||
"User Interface": "User Interface",
|
||||
"Autocomplete Delay (ms):": "Autocomplete Delay (ms):",
|
||||
"Key Backup": "Key Backup",
|
||||
"Ignored Users": "Ignored Users",
|
||||
"Submit Debug Logs": "Submit Debug Logs",
|
||||
"These are experimental features that may break in unexpected ways": "These are experimental features that may break in unexpected ways",
|
||||
"Use with caution": "Use with caution",
|
||||
"Deactivate my account": "Deactivate my account",
|
||||
"Clear Cache": "Clear Cache",
|
||||
"Updates": "Updates",
|
||||
"Bulk Options": "Bulk Options",
|
||||
"Desktop specific": "Desktop specific",
|
||||
"Missing Media Permissions, click here to request.": "Missing Media Permissions, click here to request.",
|
||||
"VoIP": "VoIP",
|
||||
"Add email address": "Add email address",
|
||||
"Display name": "Display name",
|
||||
"To return to your account in future you need to set a password": "To return to your account in future you need to set a password",
|
||||
"Logged in as:": "Logged in as:",
|
||||
"Failed to send email": "Failed to send email",
|
||||
"The email address linked to your account must be entered.": "The email address linked to your account must be entered.",
|
||||
"A new password must be entered.": "A new password must be entered.",
|
||||
"New passwords must match each other.": "New passwords must match each other.",
|
||||
"Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.",
|
||||
"An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.",
|
||||
"I have verified my email address": "I have verified my email address",
|
||||
"Your password has been reset": "Your password has been reset",
|
||||
"You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device",
|
||||
"Return to login screen": "Return to login screen",
|
||||
"To reset your password, enter the email address linked to your account": "To reset your password, enter the email address linked to your account",
|
||||
"New password": "New password",
|
||||
"Confirm your new password": "Confirm your new password",
|
||||
"Your account": "Your account",
|
||||
"Your account on %(serverName)s": "Your account on %(serverName)s",
|
||||
"The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please enter a valid URL including the protocol prefix.": "The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please enter a valid URL including the protocol prefix.",
|
||||
"A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.",
|
||||
"Send Reset Email": "Send Reset Email",
|
||||
"Sign in instead": "Sign in instead",
|
||||
"An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.",
|
||||
"I have verified my email address": "I have verified my email address",
|
||||
"Your password has been reset.": "Your password has been reset.",
|
||||
"You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device.",
|
||||
"Return to login screen": "Return to login screen",
|
||||
"Set a new password": "Set a new password",
|
||||
"Invalid homeserver discovery response": "Invalid homeserver discovery response",
|
||||
"Invalid identity server discovery response": "Invalid identity server discovery response",
|
||||
"General failure": "General failure",
|
||||
"This Home Server does not support login using email address.": "This Home Server does not support login using email address.",
|
||||
"This homeserver does not support login using email address.": "This homeserver does not support login using email address.",
|
||||
"Please <a>contact your service administrator</a> to continue using this service.": "Please <a>contact your service administrator</a> to continue using this service.",
|
||||
"Incorrect username and/or password.": "Incorrect username and/or password.",
|
||||
"Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.",
|
||||
"Guest access is disabled on this Home Server.": "Guest access is disabled on this Home Server.",
|
||||
"Guest access is disabled on this homeserver.": "Guest access is disabled on this homeserver.",
|
||||
"Failed to perform homeserver discovery": "Failed to perform homeserver discovery",
|
||||
"The phone number entered looks invalid": "The phone number entered looks invalid",
|
||||
"Unknown failure discovering homeserver": "Unknown failure discovering homeserver",
|
||||
|
@ -1413,12 +1432,12 @@
|
|||
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
|
||||
"Sign in with single sign-on": "Sign in with single sign-on",
|
||||
"Try the app first": "Try the app first",
|
||||
"Sign in to your account": "Sign in to your account",
|
||||
"Create account": "Create account",
|
||||
"Failed to fetch avatar URL": "Failed to fetch avatar URL",
|
||||
"Set a display name:": "Set a display name:",
|
||||
"Upload an avatar:": "Upload an avatar:",
|
||||
"Unable to query for supported registration methods": "Unable to query for supported registration methods",
|
||||
"Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.",
|
||||
"Unable to query for supported registration methods.": "Unable to query for supported registration methods.",
|
||||
"This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.",
|
||||
"Missing password.": "Missing password.",
|
||||
"Passwords don't match.": "Passwords don't match.",
|
||||
|
@ -1437,6 +1456,7 @@
|
|||
"Users": "Users",
|
||||
"unknown device": "unknown device",
|
||||
"NOT verified": "NOT verified",
|
||||
"Blacklisted": "Blacklisted",
|
||||
"verified": "verified",
|
||||
"Name": "Name",
|
||||
"Verification": "Verification",
|
||||
|
@ -1502,6 +1522,8 @@
|
|||
"Retry": "Retry",
|
||||
"Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.",
|
||||
"If you don't want to set this up now, you can later in Settings.": "If you don't want to set this up now, you can later in Settings.",
|
||||
"Set up": "Set up",
|
||||
"Don't ask again": "Don't ask again",
|
||||
"New Recovery Method": "New Recovery Method",
|
||||
"A new recovery passphrase and key for Secure Messages have been detected.": "A new recovery passphrase and key for Secure Messages have been detected.",
|
||||
"If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.",
|
||||
|
|
|
@ -319,8 +319,7 @@ class Handle {
|
|||
export class Distributor extends FixedDistributor {
|
||||
constructor(item, cfg) {
|
||||
super(item);
|
||||
const layout = cfg.layout;
|
||||
this._handle = layout.openHandle(item.id);
|
||||
this._handle = cfg.getLayout().openHandle(item.id);
|
||||
}
|
||||
|
||||
finish() {
|
||||
|
|
|
@ -21,7 +21,6 @@ import {
|
|||
NotificationBodyEnabledController,
|
||||
NotificationsEnabledController,
|
||||
} from "./controllers/NotificationControllers";
|
||||
import LazyLoadingController from "./controllers/LazyLoadingController";
|
||||
import CustomStatusController from "./controllers/CustomStatusController";
|
||||
|
||||
// These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
|
||||
|
@ -93,12 +92,6 @@ export const SETTINGS = {
|
|||
supportedLevels: LEVELS_FEATURE,
|
||||
default: false,
|
||||
},
|
||||
"feature_tabbed_settings": {
|
||||
isFeature: true,
|
||||
displayName: _td("Tabbed settings"),
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
default: false,
|
||||
},
|
||||
"feature_custom_status": {
|
||||
isFeature: true,
|
||||
displayName: _td("Custom user status messages"),
|
||||
|
@ -106,16 +99,9 @@ export const SETTINGS = {
|
|||
default: false,
|
||||
controller: new CustomStatusController(),
|
||||
},
|
||||
"feature_lazyloading": {
|
||||
"feature_custom_tags": {
|
||||
isFeature: true,
|
||||
displayName: _td("Increase performance by only loading room members on first view"),
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
controller: new LazyLoadingController(),
|
||||
default: true,
|
||||
},
|
||||
"feature_keybackup": {
|
||||
isFeature: true,
|
||||
displayName: _td("Backup of encryption keys to server"),
|
||||
displayName: _td("Group & filter rooms by custom tags (refresh to apply changes)"),
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
default: false,
|
||||
},
|
||||
|
@ -125,12 +111,6 @@ export const SETTINGS = {
|
|||
supportedLevels: LEVELS_FEATURE,
|
||||
default: false,
|
||||
},
|
||||
"feature_sas": {
|
||||
isFeature: true,
|
||||
displayName: _td("Two-way device verification using short text"),
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
default: false,
|
||||
},
|
||||
"MessageComposerInput.suggestEmoji": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Enable Emoji suggestions while typing'),
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 New Vector
|
||||
|
||||
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 SettingController from "./SettingController";
|
||||
import MatrixClientPeg from "../../MatrixClientPeg";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
|
||||
export default class LazyLoadingController extends SettingController {
|
||||
async onChange(level, roomId, newValue) {
|
||||
if (!PlatformPeg.get()) return;
|
||||
|
||||
MatrixClientPeg.get().stopClient();
|
||||
await MatrixClientPeg.get().store.deleteAllData();
|
||||
PlatformPeg.get().reload();
|
||||
}
|
||||
}
|
158
src/stores/CustomRoomTagStore.js
Normal file
158
src/stores/CustomRoomTagStore.js
Normal file
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
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 dis from '../dispatcher';
|
||||
import * as RoomNotifs from '../RoomNotifs';
|
||||
import RoomListStore from './RoomListStore';
|
||||
import EventEmitter from 'events';
|
||||
import { throttle } from "lodash";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
|
||||
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
|
||||
|
||||
function commonPrefix(a, b) {
|
||||
const len = Math.min(a.length, b.length);
|
||||
let prefix;
|
||||
for (let i = 0; i < len; ++i) {
|
||||
if (a.charAt(i) !== b.charAt(i)) {
|
||||
prefix = a.substr(0, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (prefix === undefined) {
|
||||
prefix = a.substr(0, len);
|
||||
}
|
||||
const spaceIdx = prefix.indexOf(' ');
|
||||
if (spaceIdx !== -1) {
|
||||
prefix = prefix.substr(0, spaceIdx + 1);
|
||||
}
|
||||
if (prefix.length >= 2) {
|
||||
return prefix;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
/**
|
||||
* A class for storing application state for ordering tags in the TagPanel.
|
||||
*/
|
||||
class CustomRoomTagStore extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
// Initialise state
|
||||
this._state = {tags: {}};
|
||||
|
||||
// as RoomListStore gets updated by every timeline event
|
||||
// throttle this to only run every 500ms
|
||||
this._getUpdatedTags = throttle(
|
||||
this._getUpdatedTags, 500, {
|
||||
leading: true,
|
||||
trailing: true,
|
||||
},
|
||||
);
|
||||
this._roomListStoreToken = RoomListStore.addListener(() => {
|
||||
this._setState({tags: this._getUpdatedTags()});
|
||||
});
|
||||
dis.register(payload => this._onDispatch(payload));
|
||||
}
|
||||
|
||||
getTags() {
|
||||
return this._state.tags;
|
||||
}
|
||||
|
||||
_setState(newState) {
|
||||
this._state = Object.assign(this._state, newState);
|
||||
this.emit("change");
|
||||
}
|
||||
|
||||
addListener(callback) {
|
||||
this.on("change", callback);
|
||||
return {
|
||||
remove: () => {
|
||||
this.removeListener("change", callback);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getSortedTags() {
|
||||
const roomLists = RoomListStore.getRoomLists();
|
||||
|
||||
const tagNames = Object.keys(this._state.tags).sort();
|
||||
const prefixes = tagNames.map((name, i) => {
|
||||
const isFirst = i === 0;
|
||||
const isLast = i === tagNames.length - 1;
|
||||
const backwardsPrefix = !isFirst ? commonPrefix(name, tagNames[i - 1]) : "";
|
||||
const forwardsPrefix = !isLast ? commonPrefix(name, tagNames[i + 1]) : "";
|
||||
const longestPrefix = backwardsPrefix.length > forwardsPrefix.length ?
|
||||
backwardsPrefix : forwardsPrefix;
|
||||
return longestPrefix;
|
||||
});
|
||||
return tagNames.map((name, i) => {
|
||||
const notifs = RoomNotifs.aggregateNotificationCount(roomLists[name]);
|
||||
let badge;
|
||||
if (notifs.count !== 0) {
|
||||
badge = notifs;
|
||||
}
|
||||
const avatarLetter = name.substr(prefixes[i].length, 1);
|
||||
const selected = this._state.tags[name];
|
||||
return {name, avatarLetter, badge, selected};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
_onDispatch(payload) {
|
||||
switch (payload.action) {
|
||||
case 'select_custom_room_tag': {
|
||||
const oldTags = this._state.tags;
|
||||
if (oldTags.hasOwnProperty(payload.tag)) {
|
||||
const tag = {};
|
||||
tag[payload.tag] = !oldTags[payload.tag];
|
||||
const tags = Object.assign({}, oldTags, tag);
|
||||
this._setState({tags});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'on_logged_out': {
|
||||
// we assume to always have a tags object in the state
|
||||
this._state = {tags: {}};
|
||||
if (this._roomListStoreToken) {
|
||||
this._roomListStoreToken.remove();
|
||||
this._roomListStoreToken = null;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_getUpdatedTags() {
|
||||
if (!SettingsStore.isFeatureEnabled("feature_custom_tags")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTagNames = Object.keys(RoomListStore.getRoomLists())
|
||||
.filter((tagName) => {
|
||||
return !tagName.match(STANDARD_TAGS_REGEX);
|
||||
}).sort();
|
||||
const prevTags = this._state && this._state.tags;
|
||||
const newTags = newTagNames.reduce((newTags, tagName) => {
|
||||
newTags[tagName] = (prevTags && prevTags[tagName]) || false;
|
||||
return newTags;
|
||||
}, {});
|
||||
return newTags;
|
||||
}
|
||||
}
|
||||
|
||||
if (global.singletonCustomRoomTagStore === undefined) {
|
||||
global.singletonCustomRoomTagStore = new CustomRoomTagStore();
|
||||
}
|
||||
export default global.singletonCustomRoomTagStore;
|
|
@ -203,6 +203,14 @@ class GroupStore extends EventEmitter {
|
|||
return this._ready[id][groupId];
|
||||
}
|
||||
|
||||
getGroupIdsForRoomId(roomId) {
|
||||
const groupIds = Object.keys(this._state[this.STATE_KEY.GroupRooms]);
|
||||
return groupIds.filter(groupId => {
|
||||
const rooms = this._state[this.STATE_KEY.GroupRooms][groupId] || [];
|
||||
return rooms.some(room => room.roomId === roomId);
|
||||
});
|
||||
}
|
||||
|
||||
getSummary(groupId) {
|
||||
return this._state[this.STATE_KEY.Summary][groupId] || {};
|
||||
}
|
||||
|
|
|
@ -202,6 +202,8 @@ class RoomListStore extends Store {
|
|||
// If somehow we dispatched a RoomListActions.tagRoom.failure before a MatrixActions.sync
|
||||
if (!this._matrixClient) return;
|
||||
|
||||
const isCustomTagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags");
|
||||
|
||||
this._matrixClient.getRooms().forEach((room, index) => {
|
||||
const myUserId = this._matrixClient.getUserId();
|
||||
const membership = room.getMyMembership();
|
||||
|
@ -224,9 +226,9 @@ class RoomListStore extends Store {
|
|||
}
|
||||
}
|
||||
|
||||
// ignore tags we don't know about
|
||||
// ignore any m. tag names we don't know about
|
||||
tagNames = tagNames.filter((t) => {
|
||||
return lists[t] !== undefined;
|
||||
return (isCustomTagsEnabled && !t.startsWith('m.')) || lists[t] !== undefined;
|
||||
});
|
||||
|
||||
if (tagNames.length) {
|
||||
|
|
|
@ -20,7 +20,6 @@ import MatrixClientPeg from '../MatrixClientPeg';
|
|||
import sdk from '../index';
|
||||
import Modal from '../Modal';
|
||||
import { _t } from '../languageHandler';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
|
||||
const INITIAL_STATE = {
|
||||
// Whether we're joining the currently viewed room (see isJoining())
|
||||
|
@ -45,8 +44,6 @@ const INITIAL_STATE = {
|
|||
forwardingEvent: null,
|
||||
|
||||
quotingEvent: null,
|
||||
|
||||
isEditingSettings: false,
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -119,28 +116,13 @@ class RoomViewStore extends Store {
|
|||
replyingToEvent: payload.event,
|
||||
});
|
||||
break;
|
||||
case 'open_room_settings':
|
||||
if (SettingsStore.isFeatureEnabled("feature_tabbed_settings")) {
|
||||
const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog");
|
||||
Modal.createTrackedDialog('Room settings', '', RoomSettingsDialog, {
|
||||
roomId: this._state.roomId,
|
||||
}, 'mx_SettingsDialog');
|
||||
} else {
|
||||
this._setState({
|
||||
isEditingSettings: true,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'open_old_room_settings':
|
||||
this._setState({
|
||||
isEditingSettings: true,
|
||||
});
|
||||
break;
|
||||
case 'close_settings':
|
||||
this._setState({
|
||||
isEditingSettings: false,
|
||||
});
|
||||
case 'open_room_settings': {
|
||||
const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog");
|
||||
Modal.createTrackedDialog('Room settings', '', RoomSettingsDialog, {
|
||||
roomId: this._state.roomId,
|
||||
}, 'mx_SettingsDialog');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -339,10 +321,6 @@ class RoomViewStore extends Store {
|
|||
return this._state.replyingToEvent;
|
||||
}
|
||||
|
||||
isEditingSettings() {
|
||||
return this._state.isEditingSettings;
|
||||
}
|
||||
|
||||
shouldPeek() {
|
||||
return this._state.shouldPeek;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,10 @@ limitations under the License.
|
|||
*/
|
||||
import {Store} from 'flux/utils';
|
||||
import dis from '../dispatcher';
|
||||
import GroupStore from './GroupStore';
|
||||
import Analytics from '../Analytics';
|
||||
import * as RoomNotifs from "../RoomNotifs";
|
||||
import MatrixClientPeg from '../MatrixClientPeg';
|
||||
|
||||
const INITIAL_STATE = {
|
||||
orderedTags: null,
|
||||
|
@ -47,7 +50,15 @@ class TagOrderStore extends Store {
|
|||
__onDispatch(payload) {
|
||||
switch (payload.action) {
|
||||
// Initialise state after initial sync
|
||||
case 'view_room': {
|
||||
const relatedGroupIds = GroupStore.getGroupIdsForRoomId(payload.room_id);
|
||||
this._updateBadges(relatedGroupIds);
|
||||
break;
|
||||
}
|
||||
case 'MatrixActions.sync': {
|
||||
if (payload.state === 'SYNCING' || payload.state === 'PREPARED') {
|
||||
this._updateBadges();
|
||||
}
|
||||
if (!(payload.prevState !== 'PREPARED' && payload.state === 'PREPARED')) {
|
||||
break;
|
||||
}
|
||||
|
@ -164,6 +175,23 @@ class TagOrderStore extends Store {
|
|||
}
|
||||
}
|
||||
|
||||
_updateBadges(groupIds = this._state.joinedGroupIds) {
|
||||
if (groupIds && groupIds.length) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const changedBadges = {};
|
||||
groupIds.forEach(groupId => {
|
||||
const rooms =
|
||||
GroupStore.getGroupRooms(groupId)
|
||||
.map(r => client.getRoom(r.roomId)) // to Room objects
|
||||
.filter(r => r !== null && r !== undefined); // filter out rooms we haven't joined from the group
|
||||
const badge = rooms && RoomNotifs.aggregateNotificationCount(rooms);
|
||||
changedBadges[groupId] = (badge && badge.count !== 0) ? badge : undefined;
|
||||
});
|
||||
const newBadges = Object.assign({}, this._state.badges, changedBadges);
|
||||
this._setState({badges: newBadges});
|
||||
}
|
||||
}
|
||||
|
||||
_updateOrderedTags() {
|
||||
this._setState({
|
||||
orderedTags:
|
||||
|
@ -190,6 +218,11 @@ class TagOrderStore extends Store {
|
|||
return tagsToKeep.concat(groupIdsToAdd);
|
||||
}
|
||||
|
||||
getGroupBadge(groupId) {
|
||||
const badges = this._state.badges;
|
||||
return badges && badges[groupId];
|
||||
}
|
||||
|
||||
getOrderedTags() {
|
||||
return this._state.orderedTags;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue