Merge branch 'develop' into bwindels/roomlistlag

This commit is contained in:
Bruno Windels 2019-02-08 16:38:57 +00:00
commit 6f9aa9c9b9
22 changed files with 245 additions and 275 deletions

View file

@ -77,7 +77,6 @@
@import "./views/elements/_Dropdown.scss";
@import "./views/elements/_EditableItemList.scss";
@import "./views/elements/_Field.scss";
@import "./views/elements/_HexVerify.scss";
@import "./views/elements/_ImageView.scss";
@import "./views/elements/_InlineSpinner.scss";
@import "./views/elements/_ManageIntegsButton.scss";
@ -156,6 +155,7 @@
@import "./views/settings/tabs/_SecuritySettingsTab.scss";
@import "./views/settings/tabs/_SettingsTab.scss";
@import "./views/settings/tabs/_VoiceSettingsTab.scss";
@import "./views/verification/_VerificationShowSas.scss";
@import "./views/voip/_CallView.scss";
@import "./views/voip/_IncomingCallbox.scss";
@import "./views/voip/_VideoView.scss";

View file

@ -21,7 +21,11 @@ limitations under the License.
.mx_CustomRoomTagPanel {
background-color: $tagpanel-bg-color;
max-height: 40%;
max-height: 40vh;
}
.mx_CustomRoomTagPanel_scroller {
max-height: inherit;
}
.mx_CustomRoomTagPanel .mx_AccessibleButton {

View file

@ -85,6 +85,7 @@ limitations under the License.
flex-grow: 1;
display: flex;
flex-direction: column;
min-height: 0; // firefox
}
.mx_TabbedView_tabPanelContent {

View file

@ -14,21 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_HexVerify {
.mx_VerificationShowSas_sas {
text-align: center;
}
.mx_HexVerify_pair {
display: inline-block;
font-weight: bold;
padding-left: 3px;
padding-right: 3px;
}
.mx_HexVerify_pair_verified {
color: $accent-color;
}
.mx_HexVerify_pair:hover{
color: $accent-color;
}

View file

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

View file

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

View file

@ -190,10 +190,13 @@ const LeftPanel = React.createClass({
const tagPanelEnabled = SettingsStore.getValue("TagPanel.enableTagPanel");
let tagPanelContainer;
const isCustomTagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags");
if (tagPanelEnabled) {
tagPanelContainer = (<div className="mx_LeftPanel_tagPanelContainer">
<TagPanel />
<CustomRoomTagPanel />
{ isCustomTagsEnabled ? <CustomRoomTagPanel /> : undefined }
<TagPanelButtons />
</div>);
}

View file

@ -19,6 +19,8 @@ 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;
@ -88,7 +90,7 @@ export default React.createClass({
});
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,
@ -107,11 +109,11 @@ export default React.createClass({
this.setState({
loading: true,
restoreError: null,
restoreType: RESTORE_TYPE_PASSPHRASE,
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,
@ -185,32 +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 &&
this.state.recoverInfo.total > 0
) {
title = _t("Error Restoring Backup");
if (this.state.restoreType === RESTORE_TYPE_RECOVERYKEY) {
content = <div>
<p>{_t(
"Backup could not be decrypted with this key: " +
"please verify that you entered the correct recovery key.",
)}</p>
</div>;
} else {
content = <div>
<p>{_t(
"Backup could not be decrypted with this passphrase: " +
"please verify that you entered the correct recovery passphrase.",
)}</p>
</div>;
}
} else if (this.state.recoverInfo) {
title = _t("Backup Restored");
let failedToDecrypt;

View file

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

View file

@ -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();

View file

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

View file

@ -172,11 +172,14 @@ module.exports = React.createClass({
this._delayedRefreshRoomList();
});
this._customTagStoreToken = CustomRoomTagStore.addListener(() => {
this.setState({
customTags: CustomRoomTagStore.getTags(),
if (SettingsStore.isFeatureEnabled("feature_custom_tags")) {
this._customTagStoreToken = CustomRoomTagStore.addListener(() => {
this.setState({
customTags: CustomRoomTagStore.getTags(),
});
});
});
}
this.refreshRoomList();
@ -728,7 +731,8 @@ module.exports = React.createClass({
];
const tagSubLists = Object.keys(this.state.lists)
.filter((tagName) => {
return this.state.customTags[tagName] && !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],

View file

@ -250,19 +250,26 @@ export default class KeyBackupPanel extends React.PureComponent {
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 />
<div>{_t("Backup version: ")}{this.state.backupInfo.version}</div>
<div>{_t("Algorithm: ")}{this.state.backupInfo.algorithm}</div>
<div>{clientBackupStatus}</div>
{uploadStatus}
<div>{backupSigStatuses}</div><br />
<br />
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
{ _t("Restore backup") }
</AccessibleButton>&nbsp;&nbsp;&nbsp;
<AccessibleButton kind="danger" onClick={this._deleteBackup}>
{ _t("Delete backup") }
</AccessibleButton>
<div>{backupSigStatuses}</div>
<div>{trustedLocally}</div>
<p>
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
{ _t("Restore backup") }
</AccessibleButton>&nbsp;&nbsp;&nbsp;
<AccessibleButton kind="danger" onClick={this._deleteBackup}>
{ _t("Delete backup") }
</AccessibleButton>
</p>
</div>;
} else {
return <div>

View file

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

View file

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

View file

@ -27,8 +27,27 @@ export default class SecurityRoomSettingsTab extends React.Component {
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 {
@ -46,19 +65,37 @@ export default class SecurityRoomSettingsTab extends React.Component {
};
_onEncryptionChange = (e) => {
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});
});
};
_fixGuestAccess = (e) => {
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) => {
@ -92,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);
@ -170,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 (
@ -218,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;

View file

@ -28,19 +28,11 @@ export default class VerificationShowSas extends React.Component {
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>
return <div className="mx_VerificationShowSas">
<p>{_t(
"Verify this user by confirming the following number appears on their screen.",
)}</p>
@ -48,15 +40,11 @@ export default class VerificationShowSas extends React.Component {
"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>
<div className="mx_VerificationShowSas_sas">
{this.props.sas}
</div>
<DialogButtons onPrimaryButtonClick={this.props.onDone}
primaryButton={_t("Continue")}
primaryDisabled={!this.state.sasVerified}
hasCancel={true}
onCancel={this.props.onCancel}
/>

View file

@ -186,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.",
@ -264,7 +270,7 @@
"Failed to join room": "Failed to join room",
"Message Pinning": "Message Pinning",
"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",
"Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)",
"Backup of encryption keys to server": "Backup of encryption keys to server",
"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",
@ -385,6 +391,7 @@
"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",
"Backup version: ": "Backup version: ",
"Algorithm: ": "Algorithm: ",
"Restore backup": "Restore backup",
@ -476,8 +483,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",
@ -1095,11 +1100,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 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.",
"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",

View file

@ -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
@ -100,12 +99,11 @@ 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"),
displayName: _td("Group & filter rooms by custom tags (refresh to apply changes)"),
supportedLevels: LEVELS_FEATURE,
controller: new LazyLoadingController(),
default: true,
default: false,
},
"feature_keybackup": {
isFeature: true,

View file

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

View file

@ -17,6 +17,8 @@ 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))$/;
@ -50,6 +52,14 @@ class CustomRoomTagStore extends EventEmitter {
// 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()});
});
@ -125,6 +135,10 @@ class CustomRoomTagStore extends EventEmitter {
}
_getUpdatedTags() {
if (!SettingsStore.isFeatureEnabled("feature_custom_tags")) {
return;
}
const newTagNames = Object.keys(RoomListStore.getRoomLists())
.filter((tagName) => {
return !tagName.match(STANDARD_TAGS_REGEX);

View file

@ -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();
@ -226,7 +228,7 @@ class RoomListStore extends Store {
// ignore any m. tag names we don't know about
tagNames = tagNames.filter((t) => {
return !t.startsWith('m.') || lists[t] !== undefined;
return (isCustomTagsEnabled && !t.startsWith('m.')) || lists[t] !== undefined;
});
if (tagNames.length) {