You've already forked matrix-react-sdk
mirror of
https://github.com/matrix-org/matrix-react-sdk.git
synced 2025-11-22 04:02:40 +03:00
Merge branches 'develop' and 't3chguy/composer_demote' of github.com:matrix-org/matrix-react-sdk into t3chguy/composer_demote
# Conflicts: # src/components/views/rooms/MessageComposer.js
This commit is contained in:
@@ -113,4 +113,29 @@ export default class BasePlatform {
|
||||
reload() {
|
||||
throw new Error("reload not implemented!");
|
||||
}
|
||||
|
||||
supportsAutoLaunch(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// XXX: Surely this should be a setting like any other?
|
||||
async getAutoLaunchEnabled(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async setAutoLaunchEnabled(enabled: boolean): void {
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
|
||||
supportsMinimizeToTray(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async getMinimizeToTrayEnabled(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async setMinimizeToTrayEnabled(enabled: boolean): void {
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import MatrixActionCreators from './actions/MatrixActionCreators';
|
||||
import {phasedRollOutExpiredForUser} from "./PhasedRollOut";
|
||||
import Modal from './Modal';
|
||||
import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
|
||||
import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
|
||||
|
||||
interface MatrixClientCreds {
|
||||
homeserverUrl: string,
|
||||
@@ -137,8 +138,9 @@ class MatrixClientPeg {
|
||||
opts.pendingEventOrdering = "detached";
|
||||
opts.lazyLoadMembers = true;
|
||||
|
||||
// Connect the matrix client to the dispatcher
|
||||
// Connect the matrix client to the dispatcher and setting handlers
|
||||
MatrixActionCreators.start(this.matrixClient);
|
||||
MatrixClientBackedSettingsHandler.matrixClient = this.matrixClient;
|
||||
|
||||
console.log(`MatrixClientPeg: really starting MatrixClient`);
|
||||
await this.get().startClient(opts);
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
const DEFAULTS = {
|
||||
export const DEFAULTS = {
|
||||
// URL to a page we show in an iframe to configure integrations
|
||||
integrations_ui_url: "https://scalar.vector.im/",
|
||||
// Base URL to the REST interface of the integrations server
|
||||
|
||||
@@ -110,6 +110,24 @@ export const CommandMap = {
|
||||
},
|
||||
}),
|
||||
|
||||
roomnick: new Command({
|
||||
name: 'roomnick',
|
||||
args: '<display_name>',
|
||||
description: _td('Changes your display nickname in the current room only'),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const ev = cli.getRoom(roomId).currentState.getStateEvents('m.room.member', cli.getUserId());
|
||||
const content = {
|
||||
...ev ? ev.getContent() : { membership: 'join' },
|
||||
displayname: args,
|
||||
};
|
||||
return success(cli.sendStateEvent(roomId, 'm.room.member', content, cli.getUserId()));
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
},
|
||||
}),
|
||||
|
||||
tint: new Command({
|
||||
name: 'tint',
|
||||
args: '<color1> [<color2>]',
|
||||
|
||||
@@ -34,6 +34,7 @@ import GroupStore from '../../stores/GroupStore';
|
||||
import FlairStore from '../../stores/FlairStore';
|
||||
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||
import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to";
|
||||
import {Group} from "matrix-js-sdk";
|
||||
|
||||
const LONG_DESC_PLACEHOLDER = _td(
|
||||
`<h1>HTML for your community's page</h1>
|
||||
@@ -569,7 +570,7 @@ export default React.createClass({
|
||||
_onShareClick: function() {
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share community dialog', '', ShareDialog, {
|
||||
target: this._matrixClient.getGroup(this.props.groupId),
|
||||
target: this._matrixClient.getGroup(this.props.groupId) || new Group(this.props.groupId),
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import dis from '../../dispatcher';
|
||||
import VectorConferenceHandler from '../../VectorConferenceHandler';
|
||||
import TagPanelButtons from './TagPanelButtons';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
import {_t} from "../../languageHandler";
|
||||
|
||||
|
||||
const LeftPanel = React.createClass({
|
||||
@@ -212,6 +213,7 @@ const LeftPanel = React.createClass({
|
||||
);
|
||||
|
||||
const searchBox = (<SearchBox
|
||||
placeholder={ _t('Filter room names') }
|
||||
onSearch={ this.onSearch }
|
||||
onCleared={ this.onSearchCleared }
|
||||
collapsed={this.props.collapsed} />);
|
||||
|
||||
@@ -525,6 +525,7 @@ module.exports = React.createClass({
|
||||
eventSendStatus={mxEv.status}
|
||||
tileShape={this.props.tileShape}
|
||||
isTwelveHour={this.props.isTwelveHour}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
last={last} isSelectedEvent={highlight} />
|
||||
</li>,
|
||||
);
|
||||
|
||||
@@ -30,6 +30,7 @@ import Promise from 'bluebird';
|
||||
import filesize from 'filesize';
|
||||
const classNames = require("classnames");
|
||||
import { _t } from '../../languageHandler';
|
||||
import {RoomPermalinkCreator} from "../../matrix-to";
|
||||
|
||||
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
const ContentMessages = require("../../ContentMessages");
|
||||
@@ -441,6 +442,11 @@ module.exports = React.createClass({
|
||||
RoomScrollStateStore.setScrollState(this.state.roomId, this._getScrollState());
|
||||
}
|
||||
|
||||
// stop tracking room changes to format permalinks
|
||||
if (this.state.permalinkCreator) {
|
||||
this.state.permalinkCreator.stop();
|
||||
}
|
||||
|
||||
if (this.refs.roomView) {
|
||||
// disconnect the D&D event listeners from the room view. This
|
||||
// is really just for hygiene - we're going to be
|
||||
@@ -537,12 +543,12 @@ module.exports = React.createClass({
|
||||
case 'picture_snapshot':
|
||||
this.uploadFile(payload.file);
|
||||
break;
|
||||
case 'notifier_enabled':
|
||||
case 'upload_failed':
|
||||
// 413: File was too big or upset the server in some way.
|
||||
if(payload.error.http_status === 413) {
|
||||
if (payload.error && payload.error.http_status === 413) {
|
||||
this._fetchMediaConfig(true);
|
||||
}
|
||||
case 'notifier_enabled':
|
||||
case 'upload_started':
|
||||
case 'upload_finished':
|
||||
this.forceUpdate();
|
||||
@@ -652,6 +658,11 @@ module.exports = React.createClass({
|
||||
this._loadMembersIfJoined(room);
|
||||
this._calculateRecommendedVersion(room);
|
||||
this._updateE2EStatus(room);
|
||||
if (!this.state.permalinkCreator) {
|
||||
const permalinkCreator = new RoomPermalinkCreator(room);
|
||||
permalinkCreator.start();
|
||||
this.setState({permalinkCreator});
|
||||
}
|
||||
},
|
||||
|
||||
_calculateRecommendedVersion: async function(room) {
|
||||
@@ -1219,6 +1230,7 @@ module.exports = React.createClass({
|
||||
searchResult={result}
|
||||
searchHighlights={this.state.searchHighlights}
|
||||
resultLink={resultLink}
|
||||
permalinkCreator={this.state.permalinkCreator}
|
||||
onWidgetLoad={onWidgetLoad} />);
|
||||
}
|
||||
return ret;
|
||||
@@ -1305,7 +1317,10 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
onSearchClick: function() {
|
||||
this.setState({ searching: true, showingPinned: false });
|
||||
this.setState({
|
||||
searching: !this.state.searching,
|
||||
showingPinned: false,
|
||||
});
|
||||
},
|
||||
|
||||
onCancelSearchClick: function() {
|
||||
@@ -1722,6 +1737,7 @@ module.exports = React.createClass({
|
||||
showApps={this.state.showApps}
|
||||
uploadAllowed={this.isFileUploadAllowed}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
permalinkCreator={this.state.permalinkCreator}
|
||||
/>;
|
||||
}
|
||||
|
||||
@@ -1823,6 +1839,7 @@ module.exports = React.createClass({
|
||||
showUrlPreview = {this.state.showUrlPreview}
|
||||
className="mx_RoomView_messagePanel"
|
||||
membersLoaded={this.state.membersLoaded}
|
||||
permalinkCreator={this.state.permalinkCreator}
|
||||
/>);
|
||||
|
||||
let topUnreadMessagesBar = null;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,12 +15,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import { _t } from '../../languageHandler';
|
||||
import PropTypes from 'prop-types';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import { throttle } from 'lodash';
|
||||
import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
||||
@@ -28,8 +26,10 @@ module.exports = React.createClass({
|
||||
displayName: 'SearchBox',
|
||||
|
||||
propTypes: {
|
||||
onSearch: React.PropTypes.func,
|
||||
onCleared: React.PropTypes.func,
|
||||
onSearch: PropTypes.func,
|
||||
onCleared: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
@@ -102,21 +102,22 @@ module.exports = React.createClass({
|
||||
const clearButton = this.state.searchTerm.length > 0 ?
|
||||
(<AccessibleButton key="button"
|
||||
className="mx_SearchBox_closeButton"
|
||||
onClick={ () => {this._clearSearch("button")} }>
|
||||
</AccessibleButton>) : undefined;
|
||||
onClick={ () => {this._clearSearch("button"); } }>
|
||||
</AccessibleButton>) : undefined;
|
||||
|
||||
const className = this.props.className || "";
|
||||
return (
|
||||
<div className="mx_SearchBox mx_textinput">
|
||||
<input
|
||||
key="searchfield"
|
||||
type="text"
|
||||
ref="search"
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
className={"mx_textinput_icon mx_textinput_search " + className}
|
||||
value={ this.state.searchTerm }
|
||||
onFocus={ this._onFocus }
|
||||
onChange={ this.onChange }
|
||||
onKeyDown={ this._onKeyDown }
|
||||
placeholder={ _t('Filter room names') }
|
||||
placeholder={ this.props.placeholder }
|
||||
/>
|
||||
{ clearButton }
|
||||
</div>
|
||||
|
||||
@@ -1202,6 +1202,7 @@ var TimelinePanel = React.createClass({
|
||||
return (
|
||||
<MessagePanel ref="messagePanel"
|
||||
room={this.props.timelineSet.room}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
hidden={this.props.hidden}
|
||||
backPaginating={this.state.backPaginating}
|
||||
forwardPaginating={forwardPaginating}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,11 +15,11 @@ 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 SyntaxHighlight from '../views/elements/SyntaxHighlight';
|
||||
import {_t} from "../../languageHandler";
|
||||
import sdk from "../../index";
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
@@ -27,31 +28,24 @@ module.exports = React.createClass({
|
||||
propTypes: {
|
||||
content: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
document.addEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
document.removeEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
|
||||
onKeyDown: function(ev) {
|
||||
if (ev.keyCode == 27) { // escape
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.props.onFinished();
|
||||
}
|
||||
roomId: PropTypes.string.isRequired,
|
||||
eventId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<div className="mx_ViewSource">
|
||||
<SyntaxHighlight className="json">
|
||||
{ JSON.stringify(this.props.content, null, 2) }
|
||||
</SyntaxHighlight>
|
||||
</div>
|
||||
<BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t('View Source')}>
|
||||
<div className="mx_ViewSource_label_left">Room ID: { this.props.roomId }</div>
|
||||
<div className="mx_ViewSource_label_right">Event ID: { this.props.eventId }</div>
|
||||
<div className="mx_ViewSource_label_bottom" />
|
||||
|
||||
<div className="mx_Dialog_content">
|
||||
<SyntaxHighlight className="json">
|
||||
{ JSON.stringify(this.props.content, null, 2) }
|
||||
</SyntaxHighlight>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -308,7 +308,19 @@ module.exports = React.createClass({
|
||||
});
|
||||
},
|
||||
|
||||
onFormValidationFailed: function(errCode) {
|
||||
onFormValidationChange: function(fieldErrors) {
|
||||
// `fieldErrors` is an object mapping field IDs to error codes when there is an
|
||||
// error or `null` for no error, so the values array will be something like:
|
||||
// `[ null, "RegistrationForm.ERR_PASSWORD_MISSING", null]`
|
||||
// Find the first non-null error code and show that.
|
||||
const errCode = Object.values(fieldErrors).find(value => !!value);
|
||||
if (!errCode) {
|
||||
this.setState({
|
||||
errorText: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let errMsg;
|
||||
switch (errCode) {
|
||||
case "RegistrationForm.ERR_PASSWORD_MISSING":
|
||||
@@ -510,7 +522,7 @@ module.exports = React.createClass({
|
||||
defaultPhoneNumber={this.state.formVals.phoneNumber}
|
||||
defaultPassword={this.state.formVals.password}
|
||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||
onError={this.onFormValidationFailed}
|
||||
onValidationChange={this.onFormValidationChange}
|
||||
onRegisterClick={this.onFormSubmit}
|
||||
onEditServerDetailsClick={onEditServerDetailsClick}
|
||||
flows={this.state.flows}
|
||||
|
||||
@@ -66,7 +66,7 @@ module.exports = React.createClass({
|
||||
}
|
||||
const scriptTag = document.createElement('script');
|
||||
scriptTag.setAttribute(
|
||||
'src', `${protocol}//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
|
||||
'src', `${protocol}//www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
|
||||
);
|
||||
this.refs.recaptchaContainer.appendChild(scriptTag);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ module.exports = React.createClass({
|
||||
defaultUsername: PropTypes.string,
|
||||
defaultPassword: PropTypes.string,
|
||||
minPasswordLength: PropTypes.number,
|
||||
onError: PropTypes.func,
|
||||
onValidationChange: PropTypes.func,
|
||||
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
|
||||
onEditServerDetailsClick: PropTypes.func,
|
||||
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
@@ -60,15 +60,14 @@ module.exports = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
minPasswordLength: 6,
|
||||
onError: function(e) {
|
||||
console.error(e);
|
||||
},
|
||||
onValidationChange: console.error,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
fieldValid: {},
|
||||
// Field error codes by field ID
|
||||
fieldErrors: {},
|
||||
// The ISO2 country code selected in the phone number entry
|
||||
phoneCountry: this.props.defaultPhoneCountry,
|
||||
};
|
||||
@@ -81,12 +80,12 @@ module.exports = React.createClass({
|
||||
// the error that ends up being displayed
|
||||
// is the one from the first invalid field.
|
||||
// It's not super ideal that this just calls
|
||||
// onError once for each invalid field.
|
||||
// onValidationChange once for each invalid field.
|
||||
this.validateField(FIELD_PHONE_NUMBER, ev.type);
|
||||
this.validateField(FIELD_EMAIL, ev.type);
|
||||
this.validateField(FIELD_PASSWORD_CONFIRM, ev.type);
|
||||
this.validateField(FIELD_PASSWORD, ev.type);
|
||||
this.validateField(FIELD_USERNAME, ev.type);
|
||||
this.validateField(FIELD_PHONE_NUMBER, ev.type);
|
||||
this.validateField(FIELD_EMAIL, ev.type);
|
||||
|
||||
const self = this;
|
||||
if (this.allFieldsValid()) {
|
||||
@@ -134,9 +133,9 @@ module.exports = React.createClass({
|
||||
* @returns {boolean} true if all fields were valid last time they were validated.
|
||||
*/
|
||||
allFieldsValid: function() {
|
||||
const keys = Object.keys(this.state.fieldValid);
|
||||
const keys = Object.keys(this.state.fieldErrors);
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
if (this.state.fieldValid[keys[i]] == false) {
|
||||
if (this.state.fieldErrors[keys[i]]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -206,21 +205,29 @@ module.exports = React.createClass({
|
||||
}
|
||||
break;
|
||||
case FIELD_PASSWORD_CONFIRM:
|
||||
this.markFieldValid(
|
||||
fieldID, pwd1 == pwd2,
|
||||
"RegistrationForm.ERR_PASSWORD_MISMATCH",
|
||||
);
|
||||
if (allowEmpty && pwd2 === "") {
|
||||
this.markFieldValid(fieldID, true);
|
||||
} else {
|
||||
this.markFieldValid(
|
||||
fieldID, pwd1 == pwd2,
|
||||
"RegistrationForm.ERR_PASSWORD_MISMATCH",
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
markFieldValid: function(fieldID, val, errorCode) {
|
||||
const fieldValid = this.state.fieldValid;
|
||||
fieldValid[fieldID] = val;
|
||||
this.setState({fieldValid: fieldValid});
|
||||
if (!val) {
|
||||
this.props.onError(errorCode);
|
||||
markFieldValid: function(fieldID, valid, errorCode) {
|
||||
const { fieldErrors } = this.state;
|
||||
if (valid) {
|
||||
fieldErrors[fieldID] = null;
|
||||
} else {
|
||||
fieldErrors[fieldID] = errorCode;
|
||||
}
|
||||
this.setState({
|
||||
fieldErrors,
|
||||
});
|
||||
this.props.onValidationChange(fieldErrors);
|
||||
},
|
||||
|
||||
fieldElementById(fieldID) {
|
||||
@@ -240,7 +247,7 @@ module.exports = React.createClass({
|
||||
|
||||
_classForField: function(fieldID, ...baseClasses) {
|
||||
let cls = baseClasses.join(' ');
|
||||
if (this.state.fieldValid[fieldID] === false) {
|
||||
if (this.state.fieldErrors[fieldID]) {
|
||||
if (cls) cls += ' ';
|
||||
cls += 'error';
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import Resend from '../../../Resend';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import {makeEventPermalink} from '../../../matrix-to';
|
||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||
|
||||
module.exports = React.createClass({
|
||||
@@ -98,6 +97,8 @@ module.exports = React.createClass({
|
||||
onViewSourceClick: function() {
|
||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
Modal.createTrackedDialog('View Event Source', '', ViewSource, {
|
||||
roomId: this.props.mxEvent.getRoomId(),
|
||||
eventId: this.props.mxEvent.getId(),
|
||||
content: this.props.mxEvent.event,
|
||||
}, 'mx_Dialog_viewsource');
|
||||
this.closeMenu();
|
||||
@@ -106,6 +107,8 @@ module.exports = React.createClass({
|
||||
onViewClearSourceClick: function() {
|
||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, {
|
||||
roomId: this.props.mxEvent.getRoomId(),
|
||||
eventId: this.props.mxEvent.getId(),
|
||||
// FIXME: _clearEvent is private
|
||||
content: this.props.mxEvent._clearEvent,
|
||||
}, 'mx_Dialog_viewsource');
|
||||
@@ -193,6 +196,7 @@ module.exports = React.createClass({
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
|
||||
target: this.props.mxEvent,
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
});
|
||||
this.closeMenu();
|
||||
},
|
||||
@@ -211,7 +215,8 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const eventStatus = this.props.mxEvent.status;
|
||||
const mxEvent = this.props.mxEvent;
|
||||
const eventStatus = mxEvent.status;
|
||||
let resendButton;
|
||||
let redactButton;
|
||||
let cancelButton;
|
||||
@@ -251,8 +256,8 @@ module.exports = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
if (isSent && this.props.mxEvent.getType() === 'm.room.message') {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (isSent && mxEvent.getType() === 'm.room.message') {
|
||||
const content = mxEvent.getContent();
|
||||
if (content.msgtype && content.msgtype !== 'm.bad.encrypted' && content.hasOwnProperty('body')) {
|
||||
forwardButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
|
||||
@@ -282,7 +287,7 @@ module.exports = React.createClass({
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.props.mxEvent.getType() !== this.props.mxEvent.getWireType()) {
|
||||
if (mxEvent.getType() !== mxEvent.getWireType()) {
|
||||
viewClearSourceButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}>
|
||||
{ _t('View Decrypted Source') }
|
||||
@@ -300,11 +305,21 @@ module.exports = React.createClass({
|
||||
}
|
||||
}
|
||||
|
||||
let permalink;
|
||||
if (this.props.permalinkCreator) {
|
||||
permalink = this.props.permalinkCreator.forEvent(
|
||||
this.props.mxEvent.getRoomId(),
|
||||
this.props.mxEvent.getId(),
|
||||
);
|
||||
}
|
||||
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
|
||||
const permalinkButton = (
|
||||
<div className="mx_MessageContextMenu_field">
|
||||
<a href={makeEventPermalink(this.props.mxEvent.getRoomId(), this.props.mxEvent.getId())}
|
||||
target="_blank" rel="noopener" onClick={this.onPermalinkClick}>{ _t('Share Message') }</a>
|
||||
<a href={permalink}
|
||||
target="_blank" rel="noopener" onClick={this.onPermalinkClick}>
|
||||
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
|
||||
? _t('Share Permalink') : _t('Share Message') }
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -318,12 +333,12 @@ module.exports = React.createClass({
|
||||
|
||||
// Bridges can provide a 'external_url' to link back to the source.
|
||||
if (
|
||||
typeof(this.props.mxEvent.event.content.external_url) === "string" &&
|
||||
isUrlPermitted(this.props.mxEvent.event.content.external_url)
|
||||
typeof(mxEvent.event.content.external_url) === "string" &&
|
||||
isUrlPermitted(mxEvent.event.content.external_url)
|
||||
) {
|
||||
externalURLButton = (
|
||||
<div className="mx_MessageContextMenu_field">
|
||||
<a href={this.props.mxEvent.event.content.external_url}
|
||||
<a href={mxEvent.event.content.external_url}
|
||||
rel="noopener" target="_blank" onClick={this.closeMenu}>{ _t('Source URL') }</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -271,6 +271,27 @@ module.exports = React.createClass({
|
||||
);
|
||||
},
|
||||
|
||||
_onClickSettings: function() {
|
||||
dis.dispatch({
|
||||
action: 'open_room_settings',
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
}
|
||||
},
|
||||
|
||||
_renderSettingsMenu: function() {
|
||||
return (
|
||||
<div>
|
||||
<div className="mx_RoomTileContextMenu_tag_field" onClick={this._onClickSettings} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icons-settings-room.svg")} width="15" height="15" />
|
||||
{ _t('Settings') }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_renderLeaveMenu: function(membership) {
|
||||
if (!membership) {
|
||||
return null;
|
||||
@@ -350,7 +371,11 @@ module.exports = React.createClass({
|
||||
|
||||
// Can't set notif level or tags on non-join rooms
|
||||
if (myMembership !== 'join') {
|
||||
return this._renderLeaveMenu(myMembership);
|
||||
return <div>
|
||||
{ this._renderLeaveMenu(myMembership) }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderSettingsMenu() }
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -360,6 +385,8 @@ module.exports = React.createClass({
|
||||
{ this._renderLeaveMenu(myMembership) }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderRoomTagMenu() }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderSettingsMenu() }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@ import sdk from '../../../index';
|
||||
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Field from "../elements/Field";
|
||||
|
||||
class DevtoolsComponent extends React.Component {
|
||||
static contextTypes = {
|
||||
@@ -56,14 +57,8 @@ class GenericEditor extends DevtoolsComponent {
|
||||
}
|
||||
|
||||
textInput(id, label) {
|
||||
return <div className="mx_DevTools_inputRow">
|
||||
<div className="mx_DevTools_inputLabelCell">
|
||||
<label htmlFor={id}>{ label }</label>
|
||||
</div>
|
||||
<div className="mx_DevTools_inputCell">
|
||||
<input id={id} className="mx_TextInputDialog_input" onChange={this._onChange} value={this.state[id]} size="32" autoFocus={true} />
|
||||
</div>
|
||||
</div>;
|
||||
return <Field id={id} label={label} size="42" autoFocus={true} type="text" autoComplete="on"
|
||||
value={this.state[id]} onChange={this._onChange} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,12 +133,8 @@ class SendCustomEvent extends GenericEditor {
|
||||
|
||||
<br />
|
||||
|
||||
<div className="mx_DevTools_inputLabelCell">
|
||||
<label htmlFor="evContent"> { _t('Event Content') } </label>
|
||||
</div>
|
||||
<div>
|
||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_DevTools_textarea" />
|
||||
</div>
|
||||
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
|
||||
autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
@@ -223,12 +214,8 @@ class SendAccountData extends GenericEditor {
|
||||
{ this.textInput('eventType', _t('Event Type')) }
|
||||
<br />
|
||||
|
||||
<div className="mx_DevTools_inputLabelCell">
|
||||
<label htmlFor="evContent"> { _t('Event Content') } </label>
|
||||
</div>
|
||||
<div>
|
||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_DevTools_textarea" />
|
||||
</div>
|
||||
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
|
||||
autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||
@@ -302,14 +289,12 @@ class FilteredList extends React.Component {
|
||||
render() {
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
return <div>
|
||||
<input size="64"
|
||||
autoFocus={true}
|
||||
onChange={this.onQuery}
|
||||
value={this.props.query}
|
||||
placeholder={_t('Filter results')}
|
||||
<Field id="DevtoolsDialog_FilteredList_filter" label={_t('Filter results')} autoFocus={true} size={64}
|
||||
type="text" autoComplete="off" value={this.props.query} onChange={this.onQuery}
|
||||
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
|
||||
// force re-render so that autoFocus is applied when this component is re-used
|
||||
key={this.props.children[0] ? this.props.children[0].key : ''} />
|
||||
|
||||
<TruncatedList getChildren={this.getChildren}
|
||||
getChildCount={this.getChildCount}
|
||||
truncateAt={this.state.truncateAt}
|
||||
|
||||
@@ -18,11 +18,12 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Tab, TabbedView} from "../../structures/TabbedView";
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import AdvancedRoomSettingsTab from "../settings/tabs/AdvancedRoomSettingsTab";
|
||||
import RolesRoomSettingsTab from "../settings/tabs/RolesRoomSettingsTab";
|
||||
import GeneralRoomSettingsTab from "../settings/tabs/GeneralRoomSettingsTab";
|
||||
import SecurityRoomSettingsTab from "../settings/tabs/SecurityRoomSettingsTab";
|
||||
import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
|
||||
import RolesRoomSettingsTab from "../settings/tabs/room/RolesRoomSettingsTab";
|
||||
import GeneralRoomSettingsTab from "../settings/tabs/room/GeneralRoomSettingsTab";
|
||||
import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsTab";
|
||||
import sdk from "../../../index";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
|
||||
export default class RoomSettingsDialog extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -60,9 +61,10 @@ export default class RoomSettingsDialog extends React.Component {
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
const roomName = MatrixClientPeg.get().getRoom(this.props.roomId).name;
|
||||
return (
|
||||
<BaseDialog className='mx_RoomSettingsDialog' hasCancel={true}
|
||||
onFinished={this.props.onFinished} title={_t("Room Settings")}>
|
||||
onFinished={this.props.onFinished} title={_t("Room Settings - %(roomName)s", {roomName})}>
|
||||
<div className='ms_SettingsDialog_content'>
|
||||
<TabbedView tabs={this._getTabs()} />
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import QRCode from 'qrcode-react';
|
||||
import {makeEventPermalink, makeGroupPermalink, makeRoomPermalink, makeUserPermalink} from "../../../matrix-to";
|
||||
import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../matrix-to";
|
||||
import * as ContextualMenu from "../../structures/ContextualMenu";
|
||||
|
||||
const socials = [
|
||||
@@ -123,6 +123,14 @@ export default class ShareDialog extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (this.props.target instanceof Room) {
|
||||
const permalinkCreator = new RoomPermalinkCreator(this.props.target);
|
||||
permalinkCreator.load();
|
||||
this.setState({permalinkCreator});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let title;
|
||||
let matrixToUrl;
|
||||
@@ -146,9 +154,9 @@ export default class ShareDialog extends React.Component {
|
||||
}
|
||||
|
||||
if (this.state.linkSpecificEvent) {
|
||||
matrixToUrl = makeEventPermalink(this.props.target.roomId, events[events.length - 1].getId());
|
||||
matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId());
|
||||
} else {
|
||||
matrixToUrl = makeRoomPermalink(this.props.target.roomId);
|
||||
matrixToUrl = this.state.permalinkCreator.forRoom();
|
||||
}
|
||||
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||
title = _t('Share User');
|
||||
@@ -169,9 +177,9 @@ export default class ShareDialog extends React.Component {
|
||||
</div>;
|
||||
|
||||
if (this.state.linkSpecificEvent) {
|
||||
matrixToUrl = makeEventPermalink(this.props.target.getRoomId(), this.props.target.getId());
|
||||
matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId());
|
||||
} else {
|
||||
matrixToUrl = makeRoomPermalink(this.props.target.getRoomId());
|
||||
matrixToUrl = this.props.permalinkCreator.forRoom();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,15 +18,15 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Tab, TabbedView} from "../../structures/TabbedView";
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import GeneralUserSettingsTab from "../settings/tabs/GeneralUserSettingsTab";
|
||||
import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import LabsSettingsTab from "../settings/tabs/LabsSettingsTab";
|
||||
import SecuritySettingsTab from "../settings/tabs/SecuritySettingsTab";
|
||||
import NotificationSettingsTab from "../settings/tabs/NotificationSettingsTab";
|
||||
import PreferencesSettingsTab from "../settings/tabs/PreferencesSettingsTab";
|
||||
import VoiceSettingsTab from "../settings/tabs/VoiceSettingsTab";
|
||||
import HelpSettingsTab from "../settings/tabs/HelpSettingsTab";
|
||||
import FlairSettingsTab from "../settings/tabs/FlairSettingsTab";
|
||||
import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab";
|
||||
import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab";
|
||||
import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserSettingsTab";
|
||||
import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab";
|
||||
import VoiceUserSettingsTab from "../settings/tabs/user/VoiceUserSettingsTab";
|
||||
import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab";
|
||||
import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab";
|
||||
import sdk from "../../../index";
|
||||
|
||||
export default class UserSettingsDialog extends React.Component {
|
||||
@@ -45,39 +45,39 @@ export default class UserSettingsDialog extends React.Component {
|
||||
tabs.push(new Tab(
|
||||
_td("Flair"),
|
||||
"mx_UserSettingsDialog_flairIcon",
|
||||
<FlairSettingsTab />,
|
||||
<FlairUserSettingsTab />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
_td("Notifications"),
|
||||
"mx_UserSettingsDialog_bellIcon",
|
||||
<NotificationSettingsTab />,
|
||||
<NotificationUserSettingsTab />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
_td("Preferences"),
|
||||
"mx_UserSettingsDialog_preferencesIcon",
|
||||
<PreferencesSettingsTab />,
|
||||
<PreferencesUserSettingsTab />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
_td("Voice & Video"),
|
||||
"mx_UserSettingsDialog_voiceIcon",
|
||||
<VoiceSettingsTab />,
|
||||
<VoiceUserSettingsTab />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
_td("Security & Privacy"),
|
||||
"mx_UserSettingsDialog_securityIcon",
|
||||
<SecuritySettingsTab />,
|
||||
<SecurityUserSettingsTab />,
|
||||
));
|
||||
if (SettingsStore.getLabsFeatures().length > 0) {
|
||||
tabs.push(new Tab(
|
||||
_td("Labs"),
|
||||
"mx_UserSettingsDialog_labsIcon",
|
||||
<LabsSettingsTab />,
|
||||
<LabsUserSettingsTab />,
|
||||
));
|
||||
}
|
||||
tabs.push(new Tab(
|
||||
_td("Help & About"),
|
||||
"mx_UserSettingsDialog_helpIcon",
|
||||
<HelpSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||
<HelpUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||
));
|
||||
|
||||
return tabs;
|
||||
|
||||
@@ -20,7 +20,7 @@ import PropTypes from 'prop-types';
|
||||
import dis from '../../../dispatcher';
|
||||
import {wantsDateSeparator} from '../../../DateUtils';
|
||||
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
|
||||
import {makeEventPermalink, makeUserPermalink} from "../../../matrix-to";
|
||||
import {makeUserPermalink} from "../../../matrix-to";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
// This component does no cycle detection, simply because the only way to make such a cycle would be to
|
||||
@@ -32,6 +32,7 @@ export default class ReplyThread extends React.Component {
|
||||
parentEv: PropTypes.instanceOf(MatrixEvent),
|
||||
// called when the ReplyThread contents has changed, including EventTiles thereof
|
||||
onWidgetLoad: PropTypes.func.isRequired,
|
||||
permalinkCreator: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@@ -85,7 +86,7 @@ export default class ReplyThread extends React.Component {
|
||||
}
|
||||
|
||||
// Part of Replies fallback support
|
||||
static getNestedReplyText(ev) {
|
||||
static getNestedReplyText(ev, permalinkCreator) {
|
||||
if (!ev) return null;
|
||||
|
||||
let {body, formatted_body: html} = ev.getContent();
|
||||
@@ -94,7 +95,7 @@ export default class ReplyThread extends React.Component {
|
||||
if (html) html = this.stripHTMLReply(html);
|
||||
}
|
||||
|
||||
const evLink = makeEventPermalink(ev.getRoomId(), ev.getId());
|
||||
const evLink = permalinkCreator.forEvent(ev.getId());
|
||||
const userLink = makeUserPermalink(ev.getSender());
|
||||
const mxid = ev.getSender();
|
||||
|
||||
@@ -159,11 +160,12 @@ export default class ReplyThread extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
static makeThread(parentEv, onWidgetLoad, ref) {
|
||||
static makeThread(parentEv, onWidgetLoad, permalinkCreator, ref) {
|
||||
if (!ReplyThread.getParentEventId(parentEv)) {
|
||||
return <div />;
|
||||
}
|
||||
return <ReplyThread parentEv={parentEv} onWidgetLoad={onWidgetLoad} ref={ref} />;
|
||||
return <ReplyThread parentEv={parentEv} onWidgetLoad={onWidgetLoad}
|
||||
ref={ref} permalinkCreator={permalinkCreator} />;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
@@ -294,6 +296,7 @@ export default class ReplyThread extends React.Component {
|
||||
<EventTile mxEvent={ev}
|
||||
tileShape="reply"
|
||||
onWidgetLoad={this.props.onWidgetLoad}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
|
||||
</blockquote>;
|
||||
});
|
||||
|
||||
@@ -68,7 +68,9 @@ export default React.createClass({
|
||||
render() {
|
||||
const GroupTile = sdk.getComponent('groups.GroupTile');
|
||||
return <div className="mx_GroupPublicity_toggle">
|
||||
<GroupTile groupId={this.props.groupId} showDescription={false} avatarHeight={40} />
|
||||
<GroupTile groupId={this.props.groupId} showDescription={false}
|
||||
avatarHeight={40} draggable={false}
|
||||
/>
|
||||
<ToggleSwitch checked={this.state.isGroupPublicised}
|
||||
disabled={!this.state.ready || this.state.busy}
|
||||
onChange={this._onPublicityToggle} />
|
||||
|
||||
@@ -33,6 +33,7 @@ const GroupTile = React.createClass({
|
||||
showDescription: PropTypes.bool,
|
||||
// Height of the group avatar in pixels
|
||||
avatarHeight: PropTypes.number,
|
||||
draggable: PropTypes.bool,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
@@ -49,6 +50,7 @@ const GroupTile = React.createClass({
|
||||
return {
|
||||
showDescription: true,
|
||||
avatarHeight: 50,
|
||||
draggable: true,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -78,54 +80,54 @@ const GroupTile = React.createClass({
|
||||
<div className="mx_GroupTile_desc">{ profile.shortDescription }</div> :
|
||||
<div />;
|
||||
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
|
||||
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
|
||||
) : null;
|
||||
profile.avatarUrl, avatarHeight, avatarHeight, "crop") : null;
|
||||
|
||||
let avatarElement = (
|
||||
<div className="mx_GroupTile_avatar">
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
idName={this.props.groupId}
|
||||
url={httpUrl}
|
||||
width={avatarHeight}
|
||||
height={avatarHeight} />
|
||||
</div>
|
||||
);
|
||||
if (this.props.draggable) {
|
||||
const avatarClone = avatarElement;
|
||||
avatarElement = (
|
||||
<Droppable droppableId="my-groups-droppable" type="draggable-TagTile">
|
||||
{ (droppableProvided, droppableSnapshot) => (
|
||||
<div ref={droppableProvided.innerRef}>
|
||||
<Draggable
|
||||
key={"GroupTile " + this.props.groupId}
|
||||
draggableId={"GroupTile " + this.props.groupId}
|
||||
index={this.props.groupId}
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div>
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
{avatarClone}
|
||||
</div>
|
||||
{ /* Instead of a blank placeholder, use a copy of the avatar itself. */ }
|
||||
{ provided.placeholder ? avatarClone : <div /> }
|
||||
</div>
|
||||
) }
|
||||
</Draggable>
|
||||
</div>
|
||||
) }
|
||||
</Droppable>
|
||||
);
|
||||
}
|
||||
|
||||
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
||||
// instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6156
|
||||
return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown} onClick={nop}>
|
||||
<Droppable droppableId="my-groups-droppable" type="draggable-TagTile">
|
||||
{ (droppableProvided, droppableSnapshot) => (
|
||||
<div ref={droppableProvided.innerRef}>
|
||||
<Draggable
|
||||
key={"GroupTile " + this.props.groupId}
|
||||
draggableId={"GroupTile " + this.props.groupId}
|
||||
index={this.props.groupId}
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div>
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<div className="mx_GroupTile_avatar">
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
idName={this.props.groupId}
|
||||
url={httpUrl}
|
||||
width={avatarHeight}
|
||||
height={avatarHeight} />
|
||||
</div>
|
||||
</div>
|
||||
{ /* Instead of a blank placeholder, use a copy of the avatar itself. */ }
|
||||
{ provided.placeholder ?
|
||||
<div className="mx_GroupTile_avatar">
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
idName={this.props.groupId}
|
||||
url={httpUrl}
|
||||
width={avatarHeight}
|
||||
height={avatarHeight} />
|
||||
</div> :
|
||||
<div />
|
||||
}
|
||||
</div>
|
||||
) }
|
||||
</Draggable>
|
||||
</div>
|
||||
) }
|
||||
</Droppable>
|
||||
{ avatarElement }
|
||||
<div className="mx_GroupTile_profile">
|
||||
<div className="mx_GroupTile_name">{ name }</div>
|
||||
{ descElement }
|
||||
|
||||
@@ -18,8 +18,9 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import dis from '../../../dispatcher';
|
||||
import { makeEventPermalink } from '../../../matrix-to';
|
||||
import { RoomPermalinkCreator } from '../../../matrix-to';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomCreate',
|
||||
@@ -47,13 +48,17 @@ module.exports = React.createClass({
|
||||
if (predecessor === undefined) {
|
||||
return <div />; // We should never have been instaniated in this case
|
||||
}
|
||||
const prevRoom = MatrixClientPeg.get().getRoom(predecessor['room_id']);
|
||||
const permalinkCreator = new RoomPermalinkCreator(prevRoom);
|
||||
permalinkCreator.load();
|
||||
const predecessorPermalink = permalinkCreator.forEvent(predecessor['event_id']);
|
||||
return <div className="mx_CreateEvent">
|
||||
<img className="mx_CreateEvent_image" src={require("../../../../res/img/room-continuation.svg")} />
|
||||
<div className="mx_CreateEvent_header">
|
||||
{_t("This room is a continuation of another conversation.")}
|
||||
</div>
|
||||
<a className="mx_CreateEvent_link"
|
||||
href={makeEventPermalink(predecessor['room_id'], predecessor['event_id'])}
|
||||
href={predecessorPermalink}
|
||||
onClick={this._onLinkClicked}
|
||||
>
|
||||
{_t("Click here to see older messages.")}
|
||||
|
||||
@@ -32,7 +32,6 @@ import withMatrixClient from '../../../wrappers/withMatrixClient';
|
||||
|
||||
const ContextualMenu = require('../../structures/ContextualMenu');
|
||||
import dis from '../../../dispatcher';
|
||||
import {makeEventPermalink} from "../../../matrix-to";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
|
||||
@@ -321,14 +320,18 @@ module.exports = withMatrixClient(React.createClass({
|
||||
|
||||
const {tile, replyThread} = this.refs;
|
||||
|
||||
let e2eInfoCallback = null;
|
||||
if (this.props.mxEvent.isEncrypted()) e2eInfoCallback = () => this.onCryptoClicked();
|
||||
|
||||
ContextualMenu.createMenu(MessageContextMenu, {
|
||||
chevronOffset: 10,
|
||||
mxEvent: this.props.mxEvent,
|
||||
left: x,
|
||||
top: y,
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
|
||||
collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
|
||||
e2eInfoCallback: () => this.onCryptoClicked(),
|
||||
e2eInfoCallback: e2eInfoCallback,
|
||||
onFinished: function() {
|
||||
self.setState({menu: false});
|
||||
},
|
||||
@@ -541,7 +544,10 @@ module.exports = withMatrixClient(React.createClass({
|
||||
mx_EventTile_redacted: isRedacted,
|
||||
});
|
||||
|
||||
const permalink = makeEventPermalink(this.props.mxEvent.getRoomId(), this.props.mxEvent.getId());
|
||||
let permalink = "#";
|
||||
if (this.props.permalinkCreator) {
|
||||
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
||||
}
|
||||
|
||||
const readAvatars = this.getReadAvatars();
|
||||
|
||||
@@ -694,6 +700,15 @@ module.exports = withMatrixClient(React.createClass({
|
||||
|
||||
case 'reply':
|
||||
case 'reply_preview': {
|
||||
let thread;
|
||||
if (this.props.tileShape === 'reply_preview') {
|
||||
thread = ReplyThread.makeThread(
|
||||
this.props.mxEvent,
|
||||
this.props.onWidgetLoad,
|
||||
this.props.permalinkCreator,
|
||||
'replyThread',
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={classes}>
|
||||
{ avatar }
|
||||
@@ -703,10 +718,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||
{ timestamp }
|
||||
</a>
|
||||
{ this._renderE2EPadlock() }
|
||||
{
|
||||
this.props.tileShape === 'reply_preview'
|
||||
&& ReplyThread.makeThread(this.props.mxEvent, this.props.onWidgetLoad, 'replyThread')
|
||||
}
|
||||
{ thread }
|
||||
<EventTileType ref="tile"
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
@@ -718,6 +730,12 @@ module.exports = withMatrixClient(React.createClass({
|
||||
);
|
||||
}
|
||||
default: {
|
||||
const thread = ReplyThread.makeThread(
|
||||
this.props.mxEvent,
|
||||
this.props.onWidgetLoad,
|
||||
this.props.permalinkCreator,
|
||||
'replyThread',
|
||||
);
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_msgOption">
|
||||
@@ -729,7 +747,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||
{ timestamp }
|
||||
</a>
|
||||
{ this._renderE2EPadlock() }
|
||||
{ ReplyThread.makeThread(this.props.mxEvent, this.props.onWidgetLoad, 'replyThread') }
|
||||
{ thread }
|
||||
<EventTileType ref="tile"
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
|
||||
@@ -339,12 +339,11 @@ module.exports = React.createClass({
|
||||
return nameA.localeCompare(nameB);
|
||||
},
|
||||
|
||||
onSearchQueryChanged: function(ev) {
|
||||
const q = ev.target.value;
|
||||
onSearchQueryChanged: function(searchQuery) {
|
||||
this.setState({
|
||||
searchQuery: q,
|
||||
filteredJoinedMembers: this._filterMembers(this.state.members, 'join', q),
|
||||
filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', q),
|
||||
searchQuery,
|
||||
filteredJoinedMembers: this._filterMembers(this.state.members, 'join', searchQuery),
|
||||
filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', searchQuery),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -438,6 +437,7 @@ module.exports = React.createClass({
|
||||
return <div className="mx_MemberList"><Spinner /></div>;
|
||||
}
|
||||
|
||||
const SearchBox = sdk.getComponent('structures.SearchBox');
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
@@ -445,7 +445,6 @@ module.exports = React.createClass({
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
let inviteButton;
|
||||
if (room && room.getMyMembership() === 'join') {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
inviteButton =
|
||||
<AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick}>
|
||||
@@ -477,9 +476,10 @@ module.exports = React.createClass({
|
||||
{ invitedSection }
|
||||
</div>
|
||||
</GeminiScrollbarWrapper>
|
||||
<input className="mx_MemberList_query mx_textinput_icon mx_textinput_search" id="mx_MemberList_query" type="text"
|
||||
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
|
||||
placeholder={_t('Filter room members')} />
|
||||
|
||||
<SearchBox className="mx_MemberList_query mx_textinput_icon mx_textinput_search"
|
||||
placeholder={ _t('Filter room members') }
|
||||
onSearch={ this.onSearchQueryChanged } />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -363,34 +363,6 @@ export default class MessageComposer extends React.Component {
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
// TODO: Remove temporary logging for riot-web#7838
|
||||
// Note: we rip apart the power level event ourselves because we don't want to
|
||||
// log too much data about it - just the bits we care about. Many of the variables
|
||||
// logged here are to help figure out where in the stack the 'cannot post in room'
|
||||
// warning is coming from. This means logging various numbers from the PL event to
|
||||
// verify RoomState._maySendEventOfType is doing the right thing.
|
||||
const room = this.props.room;
|
||||
const plEvent = room.currentState.getStateEvents('m.room.power_levels', '');
|
||||
let plEventString = "<no power level event>";
|
||||
if (plEvent) {
|
||||
const content = plEvent.getContent();
|
||||
if (!content) {
|
||||
plEventString = "<no event content>";
|
||||
} else {
|
||||
const stringifyFalsey = (v) => v === null ? '<null>' : (v === undefined ? '<undefined>' : v);
|
||||
const actualUserPl = stringifyFalsey(content.users ? content.users[room.myUserId] : "<no users in content>");
|
||||
const usersPl = stringifyFalsey(content.users_default);
|
||||
const actualEventPl = stringifyFalsey(content.events ? content.events['m.room.message'] : "<no events in content>");
|
||||
const eventPl = stringifyFalsey(content.events_default);
|
||||
plEventString = `actualUserPl=${actualUserPl} defaultUserPl=${usersPl} actualEventPl=${actualEventPl} defaultEventPl=${eventPl}`;
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
`[riot-web#7838] renderComposer() hasTombstone=${!!this.state.tombstone} maySendMessage=${room.maySendMessage()}` +
|
||||
` myMembership=${room.getMyMembership()} maySendEvent=${room.currentState.maySendEvent('m.room.message', room.myUserId)}` +
|
||||
` myUserId=${room.myUserId} roomId=${room.roomId} hasPlEvent=${!!plEvent} powerLevels='${plEventString}'`
|
||||
);
|
||||
|
||||
if (!this.state.tombstone && this.state.canSendMessages) {
|
||||
// This also currently includes the call buttons. Really we should
|
||||
// check separately for whether we can call, but this is slightly
|
||||
@@ -444,7 +416,8 @@ export default class MessageComposer extends React.Component {
|
||||
room={this.props.room}
|
||||
placeholder={placeholderText}
|
||||
onFilesPasted={this.uploadFiles}
|
||||
onInputStateChanged={this.onInputStateChanged} />,
|
||||
onInputStateChanged={this.onInputStateChanged}
|
||||
permalinkCreator={this.props.permalinkCreator} />,
|
||||
formattingButton,
|
||||
stickerpickerButton,
|
||||
uploadButton,
|
||||
@@ -470,8 +443,6 @@ export default class MessageComposer extends React.Component {
|
||||
</div>
|
||||
</div>);
|
||||
} else {
|
||||
// TODO: Remove temporary logging for riot-web#7838
|
||||
console.log("[riot-web#7838] Falling back to showing cannot post in room error");
|
||||
controls.push(
|
||||
<div key="controls_error" className="mx_MessageComposer_noperm_error">
|
||||
{ _t('You do not have permission to post to this room') }
|
||||
|
||||
@@ -1195,7 +1195,7 @@ export default class MessageComposerInput extends React.Component {
|
||||
|
||||
// Part of Replies fallback support - prepend the text we're sending
|
||||
// with the text we're replying to
|
||||
const nestedReply = ReplyThread.getNestedReplyText(replyingToEv);
|
||||
const nestedReply = ReplyThread.getNestedReplyText(replyingToEv, this.props.permalinkCreator);
|
||||
if (nestedReply) {
|
||||
if (content.formatted_body) {
|
||||
content.formatted_body = nestedReply.html + content.formatted_body;
|
||||
|
||||
@@ -56,6 +56,7 @@ module.exports = React.createClass({
|
||||
}
|
||||
if (EventTile.haveTileForEvent(ev)) {
|
||||
ret.push(<EventTile key={eventId+"+"+j} mxEvent={ev} contextual={contextual} highlights={highlights}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
highlightLink={this.props.resultLink}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />);
|
||||
}
|
||||
|
||||
@@ -170,6 +170,7 @@ module.exports = React.createClass({
|
||||
width={24}
|
||||
height={24}
|
||||
resizeMethod="crop"
|
||||
viewUserOnClick={true}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -16,11 +16,11 @@ limitations under the License.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../../languageHandler";
|
||||
import MatrixClientPeg from "../../../../MatrixClientPeg";
|
||||
import sdk from "../../../../index";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import Modal from "../../../../Modal";
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import MatrixClientPeg from "../../../../../MatrixClientPeg";
|
||||
import sdk from "../../../../..";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import Modal from "../../../../../Modal";
|
||||
|
||||
export default class AdvancedRoomSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -16,14 +16,14 @@ limitations under the License.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../../languageHandler";
|
||||
import RoomProfileSettings from "../../room_settings/RoomProfileSettings";
|
||||
import MatrixClientPeg from "../../../../MatrixClientPeg";
|
||||
import sdk from "../../../../index";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
|
||||
import MatrixClientPeg from "../../../../../MatrixClientPeg";
|
||||
import sdk from "../../../../..";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import {MatrixClient} from "matrix-js-sdk";
|
||||
import dis from "../../../../dispatcher";
|
||||
import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch";
|
||||
import dis from "../../../../../dispatcher";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
|
||||
export default class GeneralRoomSettingsTab extends React.Component {
|
||||
static childContextTypes = {
|
||||
@@ -16,11 +16,11 @@ limitations under the License.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t, _td} from "../../../../languageHandler";
|
||||
import MatrixClientPeg from "../../../../MatrixClientPeg";
|
||||
import sdk from "../../../../index";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import Modal from "../../../../Modal";
|
||||
import {_t, _td} from "../../../../../languageHandler";
|
||||
import MatrixClientPeg from "../../../../../MatrixClientPeg";
|
||||
import sdk from "../../../../..";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import Modal from "../../../../../Modal";
|
||||
|
||||
const plEventsToLabels = {
|
||||
// These will be translated for us later.
|
||||
@@ -16,11 +16,11 @@ limitations under the License.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../../languageHandler";
|
||||
import MatrixClientPeg from "../../../../MatrixClientPeg";
|
||||
import sdk from "../../../../index";
|
||||
import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch";
|
||||
import {SettingLevel} from "../../../../settings/SettingsStore";
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import MatrixClientPeg from "../../../../../MatrixClientPeg";
|
||||
import sdk from "../../../../..";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import {SettingLevel} from "../../../../../settings/SettingsStore";
|
||||
|
||||
export default class SecurityRoomSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -188,7 +188,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
|
||||
if (joinRule !== 'public' && guestAccess === 'forbidden') {
|
||||
guestWarning = (
|
||||
<div className='mx_SecurityRoomSettingsTab_warning'>
|
||||
<img src={require("../../../../../res/img/warning.svg")} width={15} height={15} />
|
||||
<img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
|
||||
<span>
|
||||
{_t("Guests cannot join this room even if explicitly invited.")}
|
||||
<a href="" onClick={this._fixGuestAccess}>{_t("Click here to fix")}</a>
|
||||
@@ -201,7 +201,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
|
||||
if (joinRule === 'public' && !hasAliases) {
|
||||
aliasWarning = (
|
||||
<div className='mx_SecurityRoomSettingsTab_warning'>
|
||||
<img src={require("../../../../../res/img/warning.svg")} width={15} height={15} />
|
||||
<img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
|
||||
<span>
|
||||
{_t("To link to this room, please add an alias.")}
|
||||
</span>
|
||||
@@ -15,14 +15,13 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {_t} from "../../../../languageHandler";
|
||||
import {DragDropContext} from "react-beautiful-dnd";
|
||||
import GroupUserSettings from "../../groups/GroupUserSettings";
|
||||
import MatrixClientPeg from "../../../../MatrixClientPeg";
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import GroupUserSettings from "../../../groups/GroupUserSettings";
|
||||
import MatrixClientPeg from "../../../../../MatrixClientPeg";
|
||||
import PropTypes from "prop-types";
|
||||
import {MatrixClient} from "matrix-js-sdk";
|
||||
|
||||
export default class FlairSettingsTab extends React.Component {
|
||||
export default class FlairUserSettingsTab extends React.Component {
|
||||
static childContextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
};
|
||||
@@ -42,9 +41,7 @@ export default class FlairSettingsTab extends React.Component {
|
||||
<div className="mx_SettingsTab">
|
||||
<span className="mx_SettingsTab_heading">{_t("Flair")}</span>
|
||||
<div className="mx_SettingsTab_section">
|
||||
<DragDropContext>
|
||||
<GroupUserSettings />
|
||||
</DragDropContext>
|
||||
<GroupUserSettings />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -15,21 +15,21 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {_t} from "../../../../languageHandler";
|
||||
import ProfileSettings from "../ProfileSettings";
|
||||
import EmailAddresses from "../EmailAddresses";
|
||||
import PhoneNumbers from "../PhoneNumbers";
|
||||
import Field from "../../elements/Field";
|
||||
import * as languageHandler from "../../../../languageHandler";
|
||||
import {SettingLevel} from "../../../../settings/SettingsStore";
|
||||
import SettingsStore from "../../../../settings/SettingsStore";
|
||||
import LanguageDropdown from "../../elements/LanguageDropdown";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import DeactivateAccountDialog from "../../dialogs/DeactivateAccountDialog";
|
||||
const PlatformPeg = require("../../../../PlatformPeg");
|
||||
const sdk = require('../../../../index');
|
||||
const Modal = require("../../../../Modal");
|
||||
const dis = require("../../../../dispatcher");
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import ProfileSettings from "../../ProfileSettings";
|
||||
import EmailAddresses from "../../EmailAddresses";
|
||||
import PhoneNumbers from "../../PhoneNumbers";
|
||||
import Field from "../../../elements/Field";
|
||||
import * as languageHandler from "../../../../../languageHandler";
|
||||
import {SettingLevel} from "../../../../../settings/SettingsStore";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import LanguageDropdown from "../../../elements/LanguageDropdown";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
|
||||
const PlatformPeg = require("../../../../../PlatformPeg");
|
||||
const sdk = require('../../../../..');
|
||||
const Modal = require("../../../../../Modal");
|
||||
const dis = require("../../../../../dispatcher");
|
||||
|
||||
export default class GeneralUserSettingsTab extends React.Component {
|
||||
constructor() {
|
||||
@@ -16,15 +16,15 @@ limitations under the License.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t, getCurrentLanguage} from "../../../../languageHandler";
|
||||
import MatrixClientPeg from "../../../../MatrixClientPeg";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import SdkConfig from "../../../../SdkConfig";
|
||||
import createRoom from "../../../../createRoom";
|
||||
const packageJson = require('../../../../../package.json');
|
||||
const Modal = require("../../../../Modal");
|
||||
const sdk = require("../../../../index");
|
||||
const PlatformPeg = require("../../../../PlatformPeg");
|
||||
import {_t, getCurrentLanguage} from "../../../../../languageHandler";
|
||||
import MatrixClientPeg from "../../../../../MatrixClientPeg";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import SdkConfig from "../../../../../SdkConfig";
|
||||
import createRoom from "../../../../../createRoom";
|
||||
const packageJson = require('../../../../../../package.json');
|
||||
const Modal = require("../../../../../Modal");
|
||||
const sdk = require("../../../../..");
|
||||
const PlatformPeg = require("../../../../../PlatformPeg");
|
||||
|
||||
// if this looks like a release, use the 'version' from package.json; else use
|
||||
// the git sha. Prepend version with v, to look like riot-web version
|
||||
@@ -45,7 +45,7 @@ const ghVersionLabel = function(repo, token='') {
|
||||
return <a target="_blank" rel="noopener" href={url}>{ token }</a>;
|
||||
};
|
||||
|
||||
export default class HelpSettingsTab extends React.Component {
|
||||
export default class HelpUserSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
closeSettingsFn: PropTypes.func.isRequired,
|
||||
};
|
||||
@@ -117,7 +117,7 @@ export default class HelpSettingsTab extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mx_SettingsTab_section mx_HelpSettingsTab_versions'>
|
||||
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Legal")}</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{legalLinks}
|
||||
@@ -190,7 +190,7 @@ export default class HelpSettingsTab extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_HelpSettingsTab">
|
||||
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Help & About")}</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className='mx_SettingsTab_subheading'>{_t('Bug reporting')}</span>
|
||||
@@ -203,12 +203,12 @@ export default class HelpSettingsTab extends React.Component {
|
||||
"other users. They do not contain messages.",
|
||||
)
|
||||
}
|
||||
<div className='mx_HelpSettingsTab_debugButton'>
|
||||
<div className='mx_HelpUserSettingsTab_debugButton'>
|
||||
<AccessibleButton onClick={this._onBugReport} kind='primary'>
|
||||
{_t("Submit debug logs")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className='mx_HelpSettingsTab_debugButton'>
|
||||
<div className='mx_HelpUserSettingsTab_debugButton'>
|
||||
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
|
||||
{_t("Clear Cache and Reload")}
|
||||
</AccessibleButton>
|
||||
@@ -221,7 +221,7 @@ export default class HelpSettingsTab extends React.Component {
|
||||
{faqText}
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx_SettingsTab_section mx_HelpSettingsTab_versions'>
|
||||
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Versions")}</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{_t("matrix-react-sdk version:")} {reactSdkVersion}<br />
|
||||
@@ -232,7 +232,7 @@ export default class HelpSettingsTab extends React.Component {
|
||||
</div>
|
||||
{this._renderLegal()}
|
||||
{this._renderCredits()}
|
||||
<div className='mx_SettingsTab_section mx_HelpSettingsTab_versions'>
|
||||
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Advanced")}</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{_t("Homeserver is")} {MatrixClientPeg.get().getHomeserverUrl()}<br />
|
||||
@@ -15,11 +15,11 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {_t} from "../../../../languageHandler";
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import PropTypes from "prop-types";
|
||||
import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore";
|
||||
import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch";
|
||||
const sdk = require("../../../../index");
|
||||
import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
const sdk = require("../../../../..");
|
||||
|
||||
export class LabsSettingToggle extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -38,7 +38,7 @@ export class LabsSettingToggle extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default class LabsSettingsTab extends React.Component {
|
||||
export default class LabsUserSettingsTab extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
@@ -15,10 +15,10 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {_t} from "../../../../languageHandler";
|
||||
const sdk = require("../../../../index");
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
const sdk = require("../../../../..");
|
||||
|
||||
export default class NotificationSettingsTab extends React.Component {
|
||||
export default class NotificationUserSettingsTab extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export default class NotificationSettingsTab extends React.Component {
|
||||
render() {
|
||||
const Notifications = sdk.getComponent("views.settings.Notifications");
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_NotificationSettingsTab">
|
||||
<div className="mx_SettingsTab mx_NotificationUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Notifications")}</div>
|
||||
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
|
||||
<Notifications />
|
||||
@@ -15,15 +15,15 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {_t} from "../../../../languageHandler";
|
||||
import {SettingLevel} from "../../../../settings/SettingsStore";
|
||||
import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch";
|
||||
import SettingsStore from "../../../../settings/SettingsStore";
|
||||
import Field from "../../elements/Field";
|
||||
const sdk = require("../../../../index");
|
||||
const PlatformPeg = require("../../../../PlatformPeg");
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import {SettingLevel} from "../../../../../settings/SettingsStore";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import Field from "../../../elements/Field";
|
||||
const sdk = require("../../../../..");
|
||||
const PlatformPeg = require("../../../../../PlatformPeg");
|
||||
|
||||
export default class PreferencesSettingsTab extends React.Component {
|
||||
export default class PreferencesUserSettingsTab extends React.Component {
|
||||
static COMPOSER_SETTINGS = [
|
||||
'MessageComposerInput.autoReplaceEmoji',
|
||||
'MessageComposerInput.suggestEmoji',
|
||||
@@ -44,6 +44,10 @@ export default class PreferencesSettingsTab extends React.Component {
|
||||
'showDisplaynameChanges',
|
||||
];
|
||||
|
||||
static ROOM_LIST_SETTINGS = [
|
||||
'RoomList.orderByImportance',
|
||||
];
|
||||
|
||||
static ADVANCED_SETTINGS = [
|
||||
'alwaysShowEncryptionIcons',
|
||||
'Pill.shouldShowPillAvatar',
|
||||
@@ -59,24 +63,39 @@ export default class PreferencesSettingsTab extends React.Component {
|
||||
this.state = {
|
||||
autoLaunch: false,
|
||||
autoLaunchSupported: false,
|
||||
minimizeToTray: true,
|
||||
minimizeToTraySupported: false,
|
||||
};
|
||||
}
|
||||
|
||||
async componentWillMount(): void {
|
||||
const autoLaunchSupported = await PlatformPeg.get().supportsAutoLaunch();
|
||||
const platform = PlatformPeg.get();
|
||||
|
||||
const autoLaunchSupported = await platform.supportsAutoLaunch();
|
||||
let autoLaunch = false;
|
||||
|
||||
if (autoLaunchSupported) {
|
||||
autoLaunch = await PlatformPeg.get().getAutoLaunchEnabled();
|
||||
autoLaunch = await platform.getAutoLaunchEnabled();
|
||||
}
|
||||
|
||||
this.setState({autoLaunch, autoLaunchSupported});
|
||||
const minimizeToTraySupported = await platform.supportsMinimizeToTray();
|
||||
let minimizeToTray = true;
|
||||
|
||||
if (minimizeToTraySupported) {
|
||||
minimizeToTray = await platform.getMinimizeToTrayEnabled();
|
||||
}
|
||||
|
||||
this.setState({autoLaunch, autoLaunchSupported, minimizeToTraySupported, minimizeToTray});
|
||||
}
|
||||
|
||||
_onAutoLaunchChange = (checked) => {
|
||||
PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({autoLaunch: checked}));
|
||||
};
|
||||
|
||||
_onMinimizeToTrayChange = (checked) => {
|
||||
PlatformPeg.get().setMinimizeToTrayEnabled(checked).then(() => this.setState({minimizeToTray: checked}));
|
||||
};
|
||||
|
||||
_onAutocompleteDelayChange = (e) => {
|
||||
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
|
||||
};
|
||||
@@ -94,18 +113,29 @@ export default class PreferencesSettingsTab extends React.Component {
|
||||
label={_t('Start automatically after system login')} />;
|
||||
}
|
||||
|
||||
let minimizeToTrayOption = null;
|
||||
if (this.state.minimizeToTraySupported) {
|
||||
minimizeToTrayOption = <LabelledToggleSwitch value={this.state.minimizeToTray}
|
||||
onChange={this._onMinimizeToTrayChange}
|
||||
label={_t('Close button should minimize window to tray')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_PreferencesSettingsTab">
|
||||
<div className="mx_SettingsTab mx_PreferencesUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Preferences")}</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
|
||||
{this._renderGroup(PreferencesSettingsTab.COMPOSER_SETTINGS)}
|
||||
{this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
|
||||
|
||||
<span className="mx_SettingsTab_subheading">{_t("Timeline")}</span>
|
||||
{this._renderGroup(PreferencesSettingsTab.TIMELINE_SETTINGS)}
|
||||
{this._renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
|
||||
|
||||
<span className="mx_SettingsTab_subheading">{_t("Room list")}</span>
|
||||
{this._renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
|
||||
|
||||
<span className="mx_SettingsTab_subheading">{_t("Advanced")}</span>
|
||||
{this._renderGroup(PreferencesSettingsTab.ADVANCED_SETTINGS)}
|
||||
{this._renderGroup(PreferencesUserSettingsTab.ADVANCED_SETTINGS)}
|
||||
{minimizeToTrayOption}
|
||||
{autoLaunchOption}
|
||||
<Field id={"autocompleteDelay"} label={_t('Autocomplete delay (ms)')} type='number'
|
||||
value={SettingsStore.getValueAt(SettingLevel.DEVICE, 'autocompleteDelay')}
|
||||
@@ -16,15 +16,15 @@ limitations under the License.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../../languageHandler";
|
||||
import {SettingLevel} from "../../../../settings/SettingsStore";
|
||||
import MatrixClientPeg from "../../../../MatrixClientPeg";
|
||||
import * as FormattingUtils from "../../../../utils/FormattingUtils";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import Analytics from "../../../../Analytics";
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import {SettingLevel} from "../../../../../settings/SettingsStore";
|
||||
import MatrixClientPeg from "../../../../../MatrixClientPeg";
|
||||
import * as FormattingUtils from "../../../../../utils/FormattingUtils";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import Analytics from "../../../../../Analytics";
|
||||
import Promise from "bluebird";
|
||||
import Modal from "../../../../Modal";
|
||||
import sdk from "../../../../index";
|
||||
import Modal from "../../../../../Modal";
|
||||
import sdk from "../../../../..";
|
||||
|
||||
export class IgnoredUser extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -38,7 +38,7 @@ export class IgnoredUser extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='mx_SecuritySettingsTab_ignoredUser'>
|
||||
<div className='mx_SecurityUserSettingsTab_ignoredUser'>
|
||||
<AccessibleButton onClick={this._onUnignoreClicked} kind='primary_sm'>
|
||||
{_t('Unignore')}
|
||||
</AccessibleButton>
|
||||
@@ -48,7 +48,7 @@ export class IgnoredUser extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default class SecuritySettingsTab extends React.Component {
|
||||
export default class SecurityUserSettingsTab extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@@ -68,14 +68,14 @@ export default class SecuritySettingsTab extends React.Component {
|
||||
|
||||
_onExportE2eKeysClicked = () => {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', '',
|
||||
import('../../../../async-components/views/dialogs/ExportE2eKeysDialog'),
|
||||
import('../../../../../async-components/views/dialogs/ExportE2eKeysDialog'),
|
||||
{matrixClient: MatrixClientPeg.get()},
|
||||
);
|
||||
};
|
||||
|
||||
_onImportE2eKeysClicked = () => {
|
||||
Modal.createTrackedDialogAsync('Import E2E Keys', '',
|
||||
import('../../../../async-components/views/dialogs/ImportE2eKeysDialog'),
|
||||
import('../../../../../async-components/views/dialogs/ImportE2eKeysDialog'),
|
||||
{matrixClient: MatrixClientPeg.get()},
|
||||
);
|
||||
};
|
||||
@@ -126,7 +126,7 @@ export default class SecuritySettingsTab extends React.Component {
|
||||
let importExportButtons = null;
|
||||
if (client.isCryptoEnabled()) {
|
||||
importExportButtons = (
|
||||
<div className='mx_SecuritySettingsTab_importExportButtons'>
|
||||
<div className='mx_SecurityUserSettingsTab_importExportButtons'>
|
||||
<AccessibleButton kind='primary' onClick={this._onExportE2eKeysClicked}>
|
||||
{_t("Export E2E room keys")}
|
||||
</AccessibleButton>
|
||||
@@ -140,7 +140,7 @@ export default class SecuritySettingsTab extends React.Component {
|
||||
return (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Cryptography")}</span>
|
||||
<ul className='mx_SettingsTab_subsectionText mx_SecuritySettingsTab_deviceInfo'>
|
||||
<ul className='mx_SettingsTab_subsectionText mx_SecurityUserSettingsTab_deviceInfo'>
|
||||
<li>
|
||||
<label>{_t("Device ID:")}</label>
|
||||
<span><code>{deviceId}</code></span>
|
||||
@@ -207,7 +207,7 @@ export default class SecuritySettingsTab extends React.Component {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_SecuritySettingsTab">
|
||||
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Devices")}</span>
|
||||
@@ -15,16 +15,16 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {_t} from "../../../../languageHandler";
|
||||
import CallMediaHandler from "../../../../CallMediaHandler";
|
||||
import Field from "../../elements/Field";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import {SettingLevel} from "../../../../settings/SettingsStore";
|
||||
const Modal = require("../../../../Modal");
|
||||
const sdk = require("../../../../index");
|
||||
const MatrixClientPeg = require("../../../../MatrixClientPeg");
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import CallMediaHandler from "../../../../../CallMediaHandler";
|
||||
import Field from "../../../elements/Field";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import {SettingLevel} from "../../../../../settings/SettingsStore";
|
||||
const Modal = require("../../../../../Modal");
|
||||
const sdk = require("../../../../..");
|
||||
const MatrixClientPeg = require("../../../../../MatrixClientPeg");
|
||||
|
||||
export default class VoiceSettingsTab extends React.Component {
|
||||
export default class VoiceUserSettingsTab extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@@ -103,7 +103,7 @@ export default class VoiceSettingsTab extends React.Component {
|
||||
let webcamDropdown = null;
|
||||
if (this.state.mediaDevices === false) {
|
||||
requestButton = (
|
||||
<div className='mx_VoiceSettingsTab_missingMediaPermissions'>
|
||||
<div className='mx_VoiceUserSettingsTab_missingMediaPermissions'>
|
||||
<p>{_t("Missing media permissions, click the button below to request.")}</p>
|
||||
<AccessibleButton onClick={this._requestMediaPermissions} kind="primary">
|
||||
{_t("Request media permissions")}
|
||||
@@ -166,7 +166,7 @@ export default class VoiceSettingsTab extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_VoiceSettingsTab">
|
||||
<div className="mx_SettingsTab mx_VoiceUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Voice & Video")}</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
{requestButton}
|
||||
@@ -132,6 +132,7 @@
|
||||
"To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.",
|
||||
"Upgrades a room to a new version": "Upgrades a room to a new version",
|
||||
"Changes your display nickname": "Changes your display nickname",
|
||||
"Changes your display nickname in the current room only": "Changes your display nickname in the current room only",
|
||||
"Changes colour scheme of current room": "Changes colour scheme of current room",
|
||||
"Gets or sets the room topic": "Gets or sets the room topic",
|
||||
"This room has no topic.": "This room has no topic.",
|
||||
@@ -306,6 +307,7 @@
|
||||
"Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets",
|
||||
"Prompt before sending invites to potentially invalid matrix IDs": "Prompt before sending invites to potentially invalid matrix IDs",
|
||||
"Show developer tools": "Show developer tools",
|
||||
"Order rooms in the room list by most important first instead of most recent": "Order rooms in the room list by most important first instead of most recent",
|
||||
"Collecting app version information": "Collecting app version information",
|
||||
"Collecting logs": "Collecting logs",
|
||||
"Uploading report": "Uploading report",
|
||||
@@ -500,19 +502,7 @@
|
||||
"Upload profile picture": "Upload profile picture",
|
||||
"Display Name": "Display Name",
|
||||
"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",
|
||||
"Room information": "Room information",
|
||||
"Internal room ID:": "Internal room ID:",
|
||||
"Room version": "Room version",
|
||||
"Room version:": "Room version:",
|
||||
"Developer options": "Developer options",
|
||||
"Open Devtools": "Open Devtools",
|
||||
"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",
|
||||
"Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them",
|
||||
@@ -528,6 +518,7 @@
|
||||
"Account management": "Account management",
|
||||
"Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!",
|
||||
"Deactivate Account": "Deactivate Account",
|
||||
"General": "General",
|
||||
"Legal": "Legal",
|
||||
"Credits": "Credits",
|
||||
"For help with using Riot, click <a>here</a>.": "For help with using Riot, click <a>here</a>.",
|
||||
@@ -551,10 +542,50 @@
|
||||
"Labs": "Labs",
|
||||
"Notifications": "Notifications",
|
||||
"Start automatically after system login": "Start automatically after system login",
|
||||
"Close button should minimize window to tray": "Close button should minimize window to tray",
|
||||
"Preferences": "Preferences",
|
||||
"Composer": "Composer",
|
||||
"Timeline": "Timeline",
|
||||
"Room list": "Room list",
|
||||
"Autocomplete delay (ms)": "Autocomplete delay (ms)",
|
||||
"Unignore": "Unignore",
|
||||
"<not supported>": "<not supported>",
|
||||
"Import E2E room keys": "Import E2E room keys",
|
||||
"Cryptography": "Cryptography",
|
||||
"Device ID:": "Device ID:",
|
||||
"Device key:": "Device key:",
|
||||
"Ignored users": "Ignored users",
|
||||
"Bulk options": "Bulk options",
|
||||
"Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites",
|
||||
"Key backup": "Key backup",
|
||||
"Security & Privacy": "Security & Privacy",
|
||||
"Devices": "Devices",
|
||||
"Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.",
|
||||
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.",
|
||||
"Learn more about how we use analytics.": "Learn more about how we use analytics.",
|
||||
"No media permissions": "No media permissions",
|
||||
"You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam",
|
||||
"Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.",
|
||||
"Request media permissions": "Request media permissions",
|
||||
"No Audio Outputs detected": "No Audio Outputs detected",
|
||||
"No Microphones detected": "No Microphones detected",
|
||||
"No Webcams detected": "No Webcams detected",
|
||||
"Default Device": "Default Device",
|
||||
"Audio Output": "Audio Output",
|
||||
"Microphone": "Microphone",
|
||||
"Camera": "Camera",
|
||||
"Voice & Video": "Voice & Video",
|
||||
"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",
|
||||
"Room information": "Room information",
|
||||
"Internal room ID:": "Internal room ID:",
|
||||
"Room version": "Room version",
|
||||
"Room version:": "Room version:",
|
||||
"Developer options": "Developer options",
|
||||
"Open Devtools": "Open Devtools",
|
||||
"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",
|
||||
"To change the room's avatar, you must be a": "To change the room's avatar, you must be a",
|
||||
"To change the room's name, you must be a": "To change the room's name, you must be a",
|
||||
"To change the room's main address, you must be a": "To change the room's main address, you must be a",
|
||||
@@ -592,38 +623,11 @@
|
||||
"Members only (since the point in time of selecting this option)": "Members only (since the point in time of selecting this option)",
|
||||
"Members only (since they were invited)": "Members only (since they were invited)",
|
||||
"Members only (since they joined)": "Members only (since they joined)",
|
||||
"Security & Privacy": "Security & Privacy",
|
||||
"Encryption": "Encryption",
|
||||
"Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.",
|
||||
"Encrypted": "Encrypted",
|
||||
"Who can access this room?": "Who can access this room?",
|
||||
"Who can read history?": "Who can read history?",
|
||||
"Unignore": "Unignore",
|
||||
"<not supported>": "<not supported>",
|
||||
"Import E2E room keys": "Import E2E room keys",
|
||||
"Cryptography": "Cryptography",
|
||||
"Device ID:": "Device ID:",
|
||||
"Device key:": "Device key:",
|
||||
"Ignored users": "Ignored users",
|
||||
"Bulk options": "Bulk options",
|
||||
"Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites",
|
||||
"Key backup": "Key backup",
|
||||
"Devices": "Devices",
|
||||
"Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.",
|
||||
"Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.",
|
||||
"Learn more about how we use analytics.": "Learn more about how we use analytics.",
|
||||
"No media permissions": "No media permissions",
|
||||
"You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam",
|
||||
"Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.",
|
||||
"Request media permissions": "Request media permissions",
|
||||
"No Audio Outputs detected": "No Audio Outputs detected",
|
||||
"No Microphones detected": "No Microphones detected",
|
||||
"No Webcams detected": "No Webcams detected",
|
||||
"Default Device": "Default Device",
|
||||
"Audio Output": "Audio Output",
|
||||
"Microphone": "Microphone",
|
||||
"Camera": "Camera",
|
||||
"Voice & Video": "Voice & Video",
|
||||
"Cannot add any more widgets": "Cannot add any more widgets",
|
||||
"The maximum permitted number of widgets have already been added to this room.": "The maximum permitted number of widgets have already been added to this room.",
|
||||
"Add a widget": "Add a widget",
|
||||
@@ -1113,7 +1117,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",
|
||||
"Room Settings": "Room Settings",
|
||||
"Room Settings - %(roomName)s": "Room Settings - %(roomName)s",
|
||||
"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",
|
||||
@@ -1202,6 +1206,7 @@
|
||||
"View Decrypted Source": "View Decrypted Source",
|
||||
"Unhide Preview": "Unhide Preview",
|
||||
"Share Message": "Share Message",
|
||||
"Share Permalink": "Share Permalink",
|
||||
"Quote": "Quote",
|
||||
"Source URL": "Source URL",
|
||||
"Collapse Reply Thread": "Collapse Reply Thread",
|
||||
|
||||
326
src/matrix-to.js
326
src/matrix-to.js
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2017 New Vector 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.
|
||||
@@ -25,17 +25,213 @@ export const baseUrl = `https://${host}`;
|
||||
// to add to permalinks. The servers are appended as ?via=example.org
|
||||
const MAX_SERVER_CANDIDATES = 3;
|
||||
|
||||
export function makeEventPermalink(roomId, eventId) {
|
||||
const permalinkBase = `${baseUrl}/#/${roomId}/${eventId}`;
|
||||
|
||||
// If the roomId isn't actually a room ID, don't try to list the servers.
|
||||
// Aliases are already routable, and don't need extra information.
|
||||
if (roomId[0] !== '!') return permalinkBase;
|
||||
// Permalinks can have servers appended to them so that the user
|
||||
// receiving them can have a fighting chance at joining the room.
|
||||
// These servers are called "candidates" at this point because
|
||||
// it is unclear whether they are going to be useful to actually
|
||||
// join in the future.
|
||||
//
|
||||
// We pick 3 servers based on the following criteria:
|
||||
//
|
||||
// Server 1: The highest power level user in the room, provided
|
||||
// they are at least PL 50. We don't calculate "what is a moderator"
|
||||
// here because it is less relevant for the vast majority of rooms.
|
||||
// We also want to ensure that we get an admin or high-ranking mod
|
||||
// as they are less likely to leave the room. If no user happens
|
||||
// to meet this criteria, we'll pick the most popular server in the
|
||||
// room.
|
||||
//
|
||||
// Server 2: The next most popular server in the room (in user
|
||||
// distribution). This cannot be the same as Server 1. If no other
|
||||
// servers are available then we'll only return Server 1.
|
||||
//
|
||||
// Server 3: The next most popular server by user distribution. This
|
||||
// has the same rules as Server 2, with the added exception that it
|
||||
// must be unique from Server 1 and 2.
|
||||
|
||||
const serverCandidates = pickServerCandidates(roomId);
|
||||
return `${permalinkBase}${encodeServerCandidates(serverCandidates)}`;
|
||||
// Rationale for popular servers: It's hard to get rid of people when
|
||||
// they keep flocking in from a particular server. Sure, the server could
|
||||
// be ACL'd in the future or for some reason be evicted from the room
|
||||
// however an event like that is unlikely the larger the room gets. If
|
||||
// the server is ACL'd at the time of generating the link however, we
|
||||
// shouldn't pick them. We also don't pick IP addresses.
|
||||
|
||||
// Note: we don't pick the server the room was created on because the
|
||||
// homeserver should already be using that server as a last ditch attempt
|
||||
// and there's less of a guarantee that the server is a resident server.
|
||||
// Instead, we actively figure out which servers are likely to be residents
|
||||
// in the future and try to use those.
|
||||
|
||||
// Note: Users receiving permalinks that happen to have all 3 potential
|
||||
// servers fail them (in terms of joining) are somewhat expected to hunt
|
||||
// down the person who gave them the link to ask for a participating server.
|
||||
// The receiving user can then manually append the known-good server to
|
||||
// the list and magically have the link work.
|
||||
|
||||
export class RoomPermalinkCreator {
|
||||
constructor(room) {
|
||||
this._room = room;
|
||||
this._highestPlUserId = null;
|
||||
this._populationMap = null;
|
||||
this._bannedHostsRegexps = null;
|
||||
this._allowedHostsRegexps = null;
|
||||
this._serverCandidates = null;
|
||||
|
||||
this.onMembership = this.onMembership.bind(this);
|
||||
this.onRoomState = this.onRoomState.bind(this);
|
||||
}
|
||||
|
||||
load() {
|
||||
this._updateAllowedServers();
|
||||
this._updateHighestPlUser();
|
||||
this._updatePopulationMap();
|
||||
this._updateServerCandidates();
|
||||
}
|
||||
|
||||
start() {
|
||||
this.load();
|
||||
this._room.on("RoomMember.membership", this.onMembership);
|
||||
this._room.on("RoomState.events", this.onRoomState);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._room.removeListener("RoomMember.membership", this.onMembership);
|
||||
this._room.removeListener("RoomState.events", this.onRoomState);
|
||||
}
|
||||
|
||||
forEvent(eventId) {
|
||||
const roomId = this._room.roomId;
|
||||
const permalinkBase = `${baseUrl}/#/${roomId}/${eventId}`;
|
||||
return `${permalinkBase}${encodeServerCandidates(this._serverCandidates)}`;
|
||||
}
|
||||
|
||||
forRoom() {
|
||||
const roomId = this._room.roomId;
|
||||
const permalinkBase = `${baseUrl}/#/${roomId}`;
|
||||
return `${permalinkBase}${encodeServerCandidates(this._serverCandidates)}`;
|
||||
}
|
||||
|
||||
onRoomState(event) {
|
||||
switch (event.getType()) {
|
||||
case "m.room.server_acl":
|
||||
this._updateAllowedServers();
|
||||
this._updateHighestPlUser();
|
||||
this._updatePopulationMap();
|
||||
this._updateServerCandidates();
|
||||
return;
|
||||
case "m.room.power_levels":
|
||||
this._updateHighestPlUser();
|
||||
this._updateServerCandidates();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onMembership(evt, member, oldMembership) {
|
||||
const userId = member.userId;
|
||||
const membership = member.membership;
|
||||
const serverName = getServerName(userId);
|
||||
const hasJoined = oldMembership !== "join" && membership === "join";
|
||||
const hasLeft = oldMembership === "join" && membership !== "join";
|
||||
|
||||
if (hasLeft) {
|
||||
this._populationMap[serverName]--;
|
||||
} else if (hasJoined) {
|
||||
this._populationMap[serverName]++;
|
||||
}
|
||||
|
||||
this._updateHighestPlUser();
|
||||
this._updateServerCandidates();
|
||||
}
|
||||
|
||||
_updateHighestPlUser() {
|
||||
const plEvent = this._room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (plEvent) {
|
||||
const content = plEvent.getContent();
|
||||
if (content) {
|
||||
const users = content.users;
|
||||
if (users) {
|
||||
const entries = Object.entries(users);
|
||||
const allowedEntries = entries.filter(([userId]) => {
|
||||
const member = this._room.getMember(userId);
|
||||
if (!member || member.membership !== "join") {
|
||||
return false;
|
||||
}
|
||||
const serverName = getServerName(userId);
|
||||
return !isHostnameIpAddress(serverName) &&
|
||||
!isHostInRegex(serverName, this._bannedHostsRegexps) &&
|
||||
isHostInRegex(serverName, this._allowedHostsRegexps);
|
||||
});
|
||||
const maxEntry = allowedEntries.reduce((max, entry) => {
|
||||
return (entry[1] > max[1]) ? entry : max;
|
||||
}, [null, 0]);
|
||||
const [userId, powerLevel] = maxEntry;
|
||||
// object wasn't empty, and max entry wasn't a demotion from the default
|
||||
if (userId !== null && powerLevel >= 50) {
|
||||
this._highestPlUserId = userId;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this._highestPlUserId = null;
|
||||
}
|
||||
|
||||
_updateAllowedServers() {
|
||||
const bannedHostsRegexps = [];
|
||||
let allowedHostsRegexps = [new RegExp(".*")]; // default allow everyone
|
||||
if (this._room.currentState) {
|
||||
const aclEvent = this._room.currentState.getStateEvents("m.room.server_acl", "");
|
||||
if (aclEvent && aclEvent.getContent()) {
|
||||
const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$");
|
||||
|
||||
const denied = aclEvent.getContent().deny || [];
|
||||
denied.forEach(h => bannedHostsRegexps.push(getRegex(h)));
|
||||
|
||||
const allowed = aclEvent.getContent().allow || [];
|
||||
allowedHostsRegexps = []; // we don't want to use the default rule here
|
||||
allowed.forEach(h => allowedHostsRegexps.push(getRegex(h)));
|
||||
}
|
||||
}
|
||||
this._bannedHostsRegexps = bannedHostsRegexps;
|
||||
this._allowedHostsRegexps = allowedHostsRegexps;
|
||||
}
|
||||
|
||||
_updatePopulationMap() {
|
||||
const populationMap: {[server:string]:number} = {};
|
||||
for (const member of this._room.getJoinedMembers()) {
|
||||
const serverName = getServerName(member.userId);
|
||||
if (!populationMap[serverName]) {
|
||||
populationMap[serverName] = 0;
|
||||
}
|
||||
populationMap[serverName]++;
|
||||
}
|
||||
this._populationMap = populationMap;
|
||||
}
|
||||
|
||||
_updateServerCandidates() {
|
||||
let candidates = [];
|
||||
if (this._highestPlUserId) {
|
||||
candidates.push(getServerName(this._highestPlUserId));
|
||||
}
|
||||
|
||||
const serversByPopulation = Object.keys(this._populationMap)
|
||||
.sort((a, b) => this._populationMap[b] - this._populationMap[a])
|
||||
.filter(a => {
|
||||
return !candidates.includes(a) &&
|
||||
!isHostnameIpAddress(a) &&
|
||||
!isHostInRegex(a, this._bannedHostsRegexps) &&
|
||||
isHostInRegex(a, this._allowedHostsRegexps);
|
||||
});
|
||||
|
||||
const remainingServers = serversByPopulation.slice(0, MAX_SERVER_CANDIDATES - candidates.length);
|
||||
candidates = candidates.concat(remainingServers);
|
||||
|
||||
this._serverCandidates = candidates;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function makeUserPermalink(userId) {
|
||||
return `${baseUrl}/#/${userId}`;
|
||||
}
|
||||
@@ -47,8 +243,14 @@ export function makeRoomPermalink(roomId) {
|
||||
// Aliases are already routable, and don't need extra information.
|
||||
if (roomId[0] !== '!') return permalinkBase;
|
||||
|
||||
const serverCandidates = pickServerCandidates(roomId);
|
||||
return `${permalinkBase}${encodeServerCandidates(serverCandidates)}`;
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(roomId);
|
||||
if (!room) {
|
||||
return permalinkBase;
|
||||
}
|
||||
const permalinkCreator = new RoomPermalinkCreator(room);
|
||||
permalinkCreator.load();
|
||||
return permalinkCreator.forRoom();
|
||||
}
|
||||
|
||||
export function makeGroupPermalink(groupId) {
|
||||
@@ -60,111 +262,13 @@ export function encodeServerCandidates(candidates) {
|
||||
return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`;
|
||||
}
|
||||
|
||||
export function pickServerCandidates(roomId) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(roomId);
|
||||
if (!room) return [];
|
||||
|
||||
// Permalinks can have servers appended to them so that the user
|
||||
// receiving them can have a fighting chance at joining the room.
|
||||
// These servers are called "candidates" at this point because
|
||||
// it is unclear whether they are going to be useful to actually
|
||||
// join in the future.
|
||||
//
|
||||
// We pick 3 servers based on the following criteria:
|
||||
//
|
||||
// Server 1: The highest power level user in the room, provided
|
||||
// they are at least PL 50. We don't calculate "what is a moderator"
|
||||
// here because it is less relevant for the vast majority of rooms.
|
||||
// We also want to ensure that we get an admin or high-ranking mod
|
||||
// as they are less likely to leave the room. If no user happens
|
||||
// to meet this criteria, we'll pick the most popular server in the
|
||||
// room.
|
||||
//
|
||||
// Server 2: The next most popular server in the room (in user
|
||||
// distribution). This cannot be the same as Server 1. If no other
|
||||
// servers are available then we'll only return Server 1.
|
||||
//
|
||||
// Server 3: The next most popular server by user distribution. This
|
||||
// has the same rules as Server 2, with the added exception that it
|
||||
// must be unique from Server 1 and 2.
|
||||
|
||||
// Rationale for popular servers: It's hard to get rid of people when
|
||||
// they keep flocking in from a particular server. Sure, the server could
|
||||
// be ACL'd in the future or for some reason be evicted from the room
|
||||
// however an event like that is unlikely the larger the room gets. If
|
||||
// the server is ACL'd at the time of generating the link however, we
|
||||
// shouldn't pick them. We also don't pick IP addresses.
|
||||
|
||||
// Note: we don't pick the server the room was created on because the
|
||||
// homeserver should already be using that server as a last ditch attempt
|
||||
// and there's less of a guarantee that the server is a resident server.
|
||||
// Instead, we actively figure out which servers are likely to be residents
|
||||
// in the future and try to use those.
|
||||
|
||||
// Note: Users receiving permalinks that happen to have all 3 potential
|
||||
// servers fail them (in terms of joining) are somewhat expected to hunt
|
||||
// down the person who gave them the link to ask for a participating server.
|
||||
// The receiving user can then manually append the known-good server to
|
||||
// the list and magically have the link work.
|
||||
|
||||
const bannedHostsRegexps = [];
|
||||
let allowedHostsRegexps = [new RegExp(".*")]; // default allow everyone
|
||||
if (room.currentState) {
|
||||
const aclEvent = room.currentState.getStateEvents("m.room.server_acl", "");
|
||||
if (aclEvent && aclEvent.getContent()) {
|
||||
const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$");
|
||||
|
||||
const denied = aclEvent.getContent().deny || [];
|
||||
denied.forEach(h => bannedHostsRegexps.push(getRegex(h)));
|
||||
|
||||
const allowed = aclEvent.getContent().allow || [];
|
||||
allowedHostsRegexps = []; // we don't want to use the default rule here
|
||||
allowed.forEach(h => allowedHostsRegexps.push(getRegex(h)));
|
||||
}
|
||||
}
|
||||
|
||||
const populationMap: {[server:string]:number} = {};
|
||||
const highestPlUser = {userId: null, powerLevel: 0, serverName: null};
|
||||
|
||||
for (const member of room.getJoinedMembers()) {
|
||||
const serverName = member.userId.split(":").splice(1).join(":");
|
||||
if (member.powerLevel > highestPlUser.powerLevel && !isHostnameIpAddress(serverName)
|
||||
&& !isHostInRegex(serverName, bannedHostsRegexps) && isHostInRegex(serverName, allowedHostsRegexps)) {
|
||||
highestPlUser.userId = member.userId;
|
||||
highestPlUser.powerLevel = member.powerLevel;
|
||||
highestPlUser.serverName = serverName;
|
||||
}
|
||||
|
||||
if (!populationMap[serverName]) populationMap[serverName] = 0;
|
||||
populationMap[serverName]++;
|
||||
}
|
||||
|
||||
const candidates = [];
|
||||
if (highestPlUser.powerLevel >= 50) candidates.push(highestPlUser.serverName);
|
||||
|
||||
const beforePopulation = candidates.length;
|
||||
const serversByPopulation = Object.keys(populationMap)
|
||||
.sort((a, b) => populationMap[b] - populationMap[a])
|
||||
.filter(a => !candidates.includes(a) && !isHostnameIpAddress(a)
|
||||
&& !isHostInRegex(a, bannedHostsRegexps) && isHostInRegex(a, allowedHostsRegexps));
|
||||
for (let i = beforePopulation; i < MAX_SERVER_CANDIDATES; i++) {
|
||||
const idx = i - beforePopulation;
|
||||
if (idx >= serversByPopulation.length) break;
|
||||
candidates.push(serversByPopulation[idx]);
|
||||
}
|
||||
|
||||
return candidates;
|
||||
function getServerName(userId) {
|
||||
return userId.split(":").splice(1).join(":");
|
||||
}
|
||||
|
||||
function getHostnameFromMatrixDomain(domain) {
|
||||
if (!domain) return null;
|
||||
|
||||
// The hostname might have a port, so we convert it to a URL and
|
||||
// split out the real hostname.
|
||||
const parser = document.createElement('a');
|
||||
parser.href = "https://" + domain;
|
||||
return parser.hostname;
|
||||
return new URL(`https://${domain}`).hostname;
|
||||
}
|
||||
|
||||
function isHostInRegex(hostname, regexps) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
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.
|
||||
@@ -340,4 +340,9 @@ export const SETTINGS = {
|
||||
displayName: _td('Show developer tools'),
|
||||
default: false,
|
||||
},
|
||||
"RoomList.orderByImportance": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Order rooms in the room list by most important first instead of most recent'),
|
||||
default: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
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.
|
||||
@@ -23,6 +24,7 @@ import RoomSettingsHandler from "./handlers/RoomSettingsHandler";
|
||||
import ConfigSettingsHandler from "./handlers/ConfigSettingsHandler";
|
||||
import {_t} from '../languageHandler';
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import dis from '../dispatcher';
|
||||
import {SETTINGS} from "./Settings";
|
||||
import LocalEchoWrapper from "./handlers/LocalEchoWrapper";
|
||||
|
||||
@@ -98,6 +100,121 @@ const LEVEL_ORDER = [
|
||||
* be enabled).
|
||||
*/
|
||||
export default class SettingsStore {
|
||||
// We support watching settings for changes, and do so only at the levels which are
|
||||
// relevant to the setting. We pass the watcher on to the handlers and aggregate it
|
||||
// before sending it off to the caller. We need to track which callback functions we
|
||||
// provide to the handlers though so we can unwatch it on demand. In practice, we
|
||||
// return a "callback reference" to the caller which correlates to an entry in this
|
||||
// dictionary for each handler's callback function.
|
||||
//
|
||||
// We also maintain a list of monitors which are special watchers: they cause dispatches
|
||||
// when the setting changes. We track which rooms we're monitoring though to ensure we
|
||||
// don't duplicate updates on the bus.
|
||||
static _watchers = {}; // { callbackRef => { level => callbackFn } }
|
||||
static _monitors = {}; // { settingName => { roomId => callbackRef } }
|
||||
|
||||
/**
|
||||
* Watches for changes in a particular setting. This is done without any local echo
|
||||
* wrapping and fires whenever a change is detected in a setting's value. Watching
|
||||
* is intended to be used in scenarios where the app needs to react to changes made
|
||||
* by other devices. It is otherwise expected that callers will be able to use the
|
||||
* Controller system or track their own changes to settings. Callers should retain
|
||||
* @param {string} settingName The setting name to watch
|
||||
* @param {String} roomId The room ID to watch for changes in. May be null for 'all'.
|
||||
* @param {function} callbackFn A function to be called when a setting change is
|
||||
* detected. Four arguments can be expected: the setting name, the room ID (may be null),
|
||||
* the level the change happened at, and finally the new value for those arguments. The
|
||||
* callback may need to do a call to #getValue() to see if a consequential change has
|
||||
* occurred.
|
||||
* @returns {string} A reference to the watcher that was employed.
|
||||
*/
|
||||
static watchSetting(settingName, roomId, callbackFn) {
|
||||
const setting = SETTINGS[settingName];
|
||||
const originalSettingName = settingName;
|
||||
if (!setting) throw new Error(`${settingName} is not a setting`);
|
||||
|
||||
if (setting.invertedSettingName) {
|
||||
settingName = setting.invertedSettingName;
|
||||
}
|
||||
|
||||
const watcherId = `${new Date().getTime()}_${settingName}_${roomId}`;
|
||||
SettingsStore._watchers[watcherId] = {};
|
||||
|
||||
const levels = Object.keys(LEVEL_HANDLERS);
|
||||
for (const level of levels) {
|
||||
const handler = SettingsStore._getHandler(originalSettingName, level);
|
||||
if (!handler) continue;
|
||||
|
||||
const localizedCallback = (changedInRoomId, newVal) => {
|
||||
callbackFn(originalSettingName, changedInRoomId, level, newVal);
|
||||
};
|
||||
|
||||
console.log(`Starting watcher for ${settingName}@${roomId || '<null room>'} at level ${level}`);
|
||||
SettingsStore._watchers[watcherId][level] = localizedCallback;
|
||||
handler.watchSetting(settingName, roomId, localizedCallback);
|
||||
}
|
||||
|
||||
return watcherId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the SettingsStore from watching a setting. This is a no-op if the watcher
|
||||
* provided is not found.
|
||||
* @param {string} watcherReference The watcher reference (received from #watchSetting)
|
||||
* to cancel.
|
||||
*/
|
||||
static unwatchSetting(watcherReference) {
|
||||
if (!SettingsStore._watchers[watcherReference]) return;
|
||||
|
||||
for (const handlerName of Object.keys(SettingsStore._watchers[watcherReference])) {
|
||||
const handler = LEVEL_HANDLERS[handlerName];
|
||||
if (!handler) continue;
|
||||
handler.unwatchSetting(SettingsStore._watchers[watcherReference][handlerName]);
|
||||
}
|
||||
|
||||
delete SettingsStore._watchers[watcherReference];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a monitor for a setting. This behaves similar to #watchSetting except instead
|
||||
* of making a call to a callback, it forwards all changes to the dispatcher. Callers can
|
||||
* expect to listen for the 'setting_updated' action with an object containing settingName,
|
||||
* roomId, level, and newValue.
|
||||
* @param {string} settingName The setting name to monitor.
|
||||
* @param {String} roomId The room ID to monitor for changes in. Use null for all rooms.
|
||||
*/
|
||||
static monitorSetting(settingName, roomId) {
|
||||
if (!this._monitors[settingName]) this._monitors[settingName] = {};
|
||||
|
||||
const registerWatcher = () => {
|
||||
this._monitors[settingName][roomId] = SettingsStore.watchSetting(
|
||||
settingName, roomId, (settingName, inRoomId, level, newValue) => {
|
||||
dis.dispatch({
|
||||
action: 'setting_updated',
|
||||
settingName,
|
||||
roomId: inRoomId,
|
||||
level,
|
||||
newValue,
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const hasRoom = Object.keys(this._monitors[settingName]).find((r) => r === roomId || r === null);
|
||||
if (!hasRoom) {
|
||||
registerWatcher();
|
||||
} else {
|
||||
if (roomId === null) {
|
||||
// Unregister all existing watchers and register the new one
|
||||
for (const roomId of Object.keys(this._monitors[settingName])) {
|
||||
SettingsStore.unwatchSetting(this._monitors[settingName][roomId]);
|
||||
}
|
||||
this._monitors[settingName] = {};
|
||||
registerWatcher();
|
||||
} // else a watcher is already registered for the room, so don't bother registering it again
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the translated display name for a given setting
|
||||
* @param {string} settingName The setting to look up.
|
||||
|
||||
57
src/settings/WatchManager.js
Normal file
57
src/settings/WatchManager.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generalized management class for dealing with watchers on a per-handler (per-level)
|
||||
* basis without duplicating code. Handlers are expected to push updates through this
|
||||
* class, which are then proxied outwards to any applicable watchers.
|
||||
*/
|
||||
export class WatchManager {
|
||||
_watchers = {}; // { settingName: { roomId: callbackFns[] } }
|
||||
|
||||
// Proxy for handlers to delegate changes to this manager
|
||||
watchSetting(settingName, roomId, cb) {
|
||||
if (!this._watchers[settingName]) this._watchers[settingName] = {};
|
||||
if (!this._watchers[settingName][roomId]) this._watchers[settingName][roomId] = [];
|
||||
this._watchers[settingName][roomId].push(cb);
|
||||
}
|
||||
|
||||
// Proxy for handlers to delegate changes to this manager
|
||||
unwatchSetting(cb) {
|
||||
for (const settingName of Object.keys(this._watchers)) {
|
||||
for (const roomId of Object.keys(this._watchers[settingName])) {
|
||||
let idx;
|
||||
while ((idx = this._watchers[settingName][roomId].indexOf(cb)) !== -1) {
|
||||
this._watchers[settingName][roomId].splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyUpdate(settingName, inRoomId, newValue) {
|
||||
if (!this._watchers[settingName]) return;
|
||||
|
||||
const roomWatchers = this._watchers[settingName];
|
||||
const callbacks = [];
|
||||
|
||||
if (inRoomId !== null && roomWatchers[inRoomId]) callbacks.push(...roomWatchers[inRoomId]);
|
||||
if (roomWatchers[null]) callbacks.push(...roomWatchers[null]);
|
||||
|
||||
for (const callback of callbacks) {
|
||||
callback(inRoomId, newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
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.
|
||||
@@ -14,14 +15,48 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import SettingsHandler from "./SettingsHandler";
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import {WatchManager} from "../WatchManager";
|
||||
import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler";
|
||||
|
||||
/**
|
||||
* Gets and sets settings at the "account" level for the current user.
|
||||
* This handler does not make use of the roomId parameter.
|
||||
*/
|
||||
export default class AccountSettingHandler extends SettingsHandler {
|
||||
export default class AccountSettingsHandler extends MatrixClientBackedSettingsHandler {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._watchers = new WatchManager();
|
||||
this._onAccountData = this._onAccountData.bind(this);
|
||||
}
|
||||
|
||||
initMatrixClient(oldClient, newClient) {
|
||||
if (oldClient) {
|
||||
oldClient.removeListener("accountData", this._onAccountData);
|
||||
}
|
||||
|
||||
newClient.on("accountData", this._onAccountData);
|
||||
}
|
||||
|
||||
_onAccountData(event) {
|
||||
if (event.getType() === "org.matrix.preview_urls") {
|
||||
let val = event.getContent()['disable'];
|
||||
if (typeof(val) !== "boolean") {
|
||||
val = null;
|
||||
} else {
|
||||
val = !val;
|
||||
}
|
||||
|
||||
this._watchers.notifyUpdate("urlPreviewsEnabled", null, val);
|
||||
} else if (event.getType() === "im.vector.web.settings") {
|
||||
// We can't really discern what changed, so trigger updates for everything
|
||||
for (const settingName of Object.keys(event.getContent())) {
|
||||
this._watchers.notifyUpdate(settingName, null, event.getContent()[settingName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getValue(settingName, roomId) {
|
||||
// Special case URL previews
|
||||
if (settingName === "urlPreviewsEnabled") {
|
||||
@@ -67,6 +102,14 @@ export default class AccountSettingHandler extends SettingsHandler {
|
||||
return cli !== undefined && cli !== null;
|
||||
}
|
||||
|
||||
watchSetting(settingName, roomId, cb) {
|
||||
this._watchers.watchSetting(settingName, roomId, cb);
|
||||
}
|
||||
|
||||
unwatchSetting(cb) {
|
||||
this._watchers.unwatchSetting(cb);
|
||||
}
|
||||
|
||||
_getSettings(eventType = "im.vector.web.settings") {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli) return null;
|
||||
|
||||
@@ -47,4 +47,12 @@ export default class ConfigSettingsHandler extends SettingsHandler {
|
||||
isSupported() {
|
||||
return true; // SdkConfig is always there
|
||||
}
|
||||
|
||||
watchSetting(settingName, roomId, cb) {
|
||||
// no-op: no changes possible
|
||||
}
|
||||
|
||||
unwatchSetting(cb) {
|
||||
// no-op: no changes possible
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
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.
|
||||
@@ -51,4 +52,12 @@ export default class DefaultSettingsHandler extends SettingsHandler {
|
||||
isSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
watchSetting(settingName, roomId, cb) {
|
||||
// no-op: no changes possible
|
||||
}
|
||||
|
||||
unwatchSetting(cb) {
|
||||
// no-op: no changes possible
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
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.
|
||||
@@ -17,6 +18,7 @@ limitations under the License.
|
||||
import Promise from 'bluebird';
|
||||
import SettingsHandler from "./SettingsHandler";
|
||||
import MatrixClientPeg from "../../MatrixClientPeg";
|
||||
import {WatchManager} from "../WatchManager";
|
||||
|
||||
/**
|
||||
* Gets and sets settings at the "device" level for the current device.
|
||||
@@ -31,6 +33,7 @@ export default class DeviceSettingsHandler extends SettingsHandler {
|
||||
constructor(featureNames) {
|
||||
super();
|
||||
this._featureNames = featureNames;
|
||||
this._watchers = new WatchManager();
|
||||
}
|
||||
|
||||
getValue(settingName, roomId) {
|
||||
@@ -66,18 +69,22 @@ export default class DeviceSettingsHandler extends SettingsHandler {
|
||||
// Special case notifications
|
||||
if (settingName === "notificationsEnabled") {
|
||||
localStorage.setItem("notifications_enabled", newValue);
|
||||
this._watchers.notifyUpdate(settingName, null, newValue);
|
||||
return Promise.resolve();
|
||||
} else if (settingName === "notificationBodyEnabled") {
|
||||
localStorage.setItem("notifications_body_enabled", newValue);
|
||||
this._watchers.notifyUpdate(settingName, null, newValue);
|
||||
return Promise.resolve();
|
||||
} else if (settingName === "audioNotificationsEnabled") {
|
||||
localStorage.setItem("audio_notifications_enabled", newValue);
|
||||
this._watchers.notifyUpdate(settingName, null, newValue);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const settings = this._getSettings() || {};
|
||||
settings[settingName] = newValue;
|
||||
localStorage.setItem("mx_local_settings", JSON.stringify(settings));
|
||||
this._watchers.notifyUpdate(settingName, null, newValue);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -90,6 +97,14 @@ export default class DeviceSettingsHandler extends SettingsHandler {
|
||||
return localStorage !== undefined && localStorage !== null;
|
||||
}
|
||||
|
||||
watchSetting(settingName, roomId, cb) {
|
||||
this._watchers.watchSetting(settingName, roomId, cb);
|
||||
}
|
||||
|
||||
unwatchSetting(cb) {
|
||||
this._watchers.unwatchSetting(cb);
|
||||
}
|
||||
|
||||
_getSettings() {
|
||||
const value = localStorage.getItem("mx_local_settings");
|
||||
if (!value) return null;
|
||||
@@ -111,5 +126,6 @@ export default class DeviceSettingsHandler extends SettingsHandler {
|
||||
|
||||
_writeFeature(featureName, enabled) {
|
||||
localStorage.setItem("mx_labs_feature_" + featureName, enabled);
|
||||
this._watchers.notifyUpdate(featureName, null, enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
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.
|
||||
@@ -66,4 +67,12 @@ export default class LocalEchoWrapper extends SettingsHandler {
|
||||
isSupported() {
|
||||
return this._handler.isSupported();
|
||||
}
|
||||
|
||||
watchSetting(settingName, roomId, cb) {
|
||||
this._handler.watchSetting(settingName, roomId, cb);
|
||||
}
|
||||
|
||||
unwatchSetting(cb) {
|
||||
this._handler.unwatchSetting(cb);
|
||||
}
|
||||
}
|
||||
|
||||
48
src/settings/handlers/MatrixClientBackedSettingsHandler.js
Normal file
48
src/settings/handlers/MatrixClientBackedSettingsHandler.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
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 SettingsHandler from "./SettingsHandler";
|
||||
|
||||
// Dev note: This whole class exists in the event someone logs out and back in - we want
|
||||
// to make sure the right MatrixClient is listening for changes.
|
||||
|
||||
/**
|
||||
* Represents the base class for settings handlers which need access to a MatrixClient.
|
||||
* This class performs no logic and should be overridden.
|
||||
*/
|
||||
export default class MatrixClientBackedSettingsHandler extends SettingsHandler {
|
||||
static _matrixClient;
|
||||
static _instances = [];
|
||||
|
||||
static set matrixClient(client) {
|
||||
const oldClient = MatrixClientBackedSettingsHandler._matrixClient;
|
||||
MatrixClientBackedSettingsHandler._matrixClient = client;
|
||||
|
||||
for (const instance of MatrixClientBackedSettingsHandler._instances) {
|
||||
instance.initMatrixClient(oldClient, client);
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
MatrixClientBackedSettingsHandler._instances.push(this);
|
||||
}
|
||||
|
||||
initMatrixClient() {
|
||||
console.warn("initMatrixClient not overridden");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
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.
|
||||
@@ -14,13 +15,51 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import SettingsHandler from "./SettingsHandler";
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler";
|
||||
import {WatchManager} from "../WatchManager";
|
||||
|
||||
/**
|
||||
* Gets and sets settings at the "room-account" level for the current user.
|
||||
*/
|
||||
export default class RoomAccountSettingsHandler extends SettingsHandler {
|
||||
export default class RoomAccountSettingsHandler extends MatrixClientBackedSettingsHandler {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._watchers = new WatchManager();
|
||||
this._onAccountData = this._onAccountData.bind(this);
|
||||
}
|
||||
|
||||
initMatrixClient(oldClient, newClient) {
|
||||
if (oldClient) {
|
||||
oldClient.removeListener("Room.accountData", this._onAccountData);
|
||||
}
|
||||
|
||||
newClient.on("Room.accountData", this._onAccountData);
|
||||
}
|
||||
|
||||
_onAccountData(event, room) {
|
||||
const roomId = room.roomId;
|
||||
|
||||
if (event.getType() === "org.matrix.room.preview_urls") {
|
||||
let val = event.getContent()['disable'];
|
||||
if (typeof (val) !== "boolean") {
|
||||
val = null;
|
||||
} else {
|
||||
val = !val;
|
||||
}
|
||||
|
||||
this._watchers.notifyUpdate("urlPreviewsEnabled", roomId, val);
|
||||
} else if (event.getType() === "org.matrix.room.color_scheme") {
|
||||
this._watchers.notifyUpdate("roomColor", roomId, event.getContent());
|
||||
} else if (event.getType() === "im.vector.web.settings") {
|
||||
// We can't really discern what changed, so trigger updates for everything
|
||||
for (const settingName of Object.keys(event.getContent())) {
|
||||
this._watchers.notifyUpdate(settingName, roomId, event.getContent()[settingName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getValue(settingName, roomId) {
|
||||
// Special case URL previews
|
||||
if (settingName === "urlPreviewsEnabled") {
|
||||
@@ -74,6 +113,14 @@ export default class RoomAccountSettingsHandler extends SettingsHandler {
|
||||
return cli !== undefined && cli !== null;
|
||||
}
|
||||
|
||||
watchSetting(settingName, roomId, cb) {
|
||||
this._watchers.watchSetting(settingName, roomId, cb);
|
||||
}
|
||||
|
||||
unwatchSetting(cb) {
|
||||
this._watchers.unwatchSetting(cb);
|
||||
}
|
||||
|
||||
_getSettings(roomId, eventType = "im.vector.web.settings") {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) return null;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
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.
|
||||
@@ -16,12 +17,19 @@ limitations under the License.
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import SettingsHandler from "./SettingsHandler";
|
||||
import {WatchManager} from "../WatchManager";
|
||||
|
||||
/**
|
||||
* Gets and sets settings at the "room-device" level for the current device in a particular
|
||||
* room.
|
||||
*/
|
||||
export default class RoomDeviceSettingsHandler extends SettingsHandler {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._watchers = new WatchManager();
|
||||
}
|
||||
|
||||
getValue(settingName, roomId) {
|
||||
// Special case blacklist setting to use legacy values
|
||||
if (settingName === "blacklistUnverifiedDevices") {
|
||||
@@ -44,6 +52,7 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler {
|
||||
if (!value["blacklistUnverifiedDevicesPerRoom"]) value["blacklistUnverifiedDevicesPerRoom"] = {};
|
||||
value["blacklistUnverifiedDevicesPerRoom"][roomId] = newValue;
|
||||
localStorage.setItem("mx_local_settings", JSON.stringify(value));
|
||||
this._watchers.notifyUpdate(settingName, roomId, newValue);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -54,6 +63,7 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler {
|
||||
localStorage.setItem(this._getKey(settingName, roomId), newValue);
|
||||
}
|
||||
|
||||
this._watchers.notifyUpdate(settingName, roomId, newValue);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -65,6 +75,14 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler {
|
||||
return localStorage !== undefined && localStorage !== null;
|
||||
}
|
||||
|
||||
watchSetting(settingName, roomId, cb) {
|
||||
this._watchers.watchSetting(settingName, roomId, cb);
|
||||
}
|
||||
|
||||
unwatchSetting(cb) {
|
||||
this._watchers.unwatchSetting(cb);
|
||||
}
|
||||
|
||||
_read(key) {
|
||||
const rawValue = localStorage.getItem(key);
|
||||
if (!rawValue) return null;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
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.
|
||||
@@ -14,13 +15,49 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import SettingsHandler from "./SettingsHandler";
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler";
|
||||
import {WatchManager} from "../WatchManager";
|
||||
|
||||
/**
|
||||
* Gets and sets settings at the "room" level.
|
||||
*/
|
||||
export default class RoomSettingsHandler extends SettingsHandler {
|
||||
export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandler {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._watchers = new WatchManager();
|
||||
this._onEvent = this._onEvent.bind(this);
|
||||
}
|
||||
|
||||
initMatrixClient(oldClient, newClient) {
|
||||
if (oldClient) {
|
||||
oldClient.removeListener("RoomState.events", this._onEvent);
|
||||
}
|
||||
|
||||
newClient.on("RoomState.events", this._onEvent);
|
||||
}
|
||||
|
||||
_onEvent(event) {
|
||||
const roomId = event.getRoomId();
|
||||
|
||||
if (event.getType() === "org.matrix.room.preview_urls") {
|
||||
let val = event.getContent()['disable'];
|
||||
if (typeof (val) !== "boolean") {
|
||||
val = null;
|
||||
} else {
|
||||
val = !val;
|
||||
}
|
||||
|
||||
this._watchers.notifyUpdate("urlPreviewsEnabled", roomId, val);
|
||||
} else if (event.getType() === "im.vector.web.settings") {
|
||||
// We can't really discern what changed, so trigger updates for everything
|
||||
for (const settingName of Object.keys(event.getContent())) {
|
||||
this._watchers.notifyUpdate(settingName, roomId, event.getContent()[settingName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getValue(settingName, roomId) {
|
||||
// Special case URL previews
|
||||
if (settingName === "urlPreviewsEnabled") {
|
||||
@@ -64,6 +101,14 @@ export default class RoomSettingsHandler extends SettingsHandler {
|
||||
return cli !== undefined && cli !== null;
|
||||
}
|
||||
|
||||
watchSetting(settingName, roomId, cb) {
|
||||
this._watchers.watchSetting(settingName, roomId, cb);
|
||||
}
|
||||
|
||||
unwatchSetting(cb) {
|
||||
this._watchers.unwatchSetting(cb);
|
||||
}
|
||||
|
||||
_getSettings(roomId, eventType = "im.vector.web.settings") {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) return null;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
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.
|
||||
@@ -68,4 +69,27 @@ export default class SettingsHandler {
|
||||
isSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches for a setting change within this handler. The caller should preserve
|
||||
* a reference to the callback so that it may be unwatched. The caller should
|
||||
* additionally provide a unique callback for multiple watchers on the same setting.
|
||||
* @param {string} settingName The setting name to watch for changes in.
|
||||
* @param {String} roomId The room ID to watch for changes in.
|
||||
* @param {function} cb A function taking two arguments: the room ID the setting changed
|
||||
* in and the new value for the setting at this level in the given room.
|
||||
*/
|
||||
watchSetting(settingName, roomId, cb) {
|
||||
throw new Error("Invalid operation: watchSetting was not overridden");
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwatches a previously watched setting. If the callback is not associated with
|
||||
* a watcher, this is a no-op.
|
||||
* @param {function} cb A callback function previously supplied to watchSetting
|
||||
* which should no longer be used.
|
||||
*/
|
||||
unwatchSetting(cb) {
|
||||
throw new Error("Invalid operation: unwatchSetting was not overridden");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,22 @@ class RoomListStore extends Store {
|
||||
this._recentsComparator = this._recentsComparator.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alerts the RoomListStore to a potential change in how room list sorting should
|
||||
* behave.
|
||||
* @param {boolean} forceRegeneration True to force a change in the algorithm
|
||||
*/
|
||||
updateSortingAlgorithm(forceRegeneration = false) {
|
||||
const byImportance = SettingsStore.getValue("RoomList.orderByImportance");
|
||||
if (byImportance !== this._state.orderRoomsByImportance || forceRegeneration) {
|
||||
console.log("Updating room sorting algorithm: sortByImportance=" + byImportance);
|
||||
this._setState({orderRoomsByImportance: byImportance});
|
||||
|
||||
// Trigger a resort of the entire list to reflect the change in algorithm
|
||||
this._generateInitialRoomLists();
|
||||
}
|
||||
}
|
||||
|
||||
_init() {
|
||||
// Initialise state
|
||||
const defaultLists = {
|
||||
@@ -77,7 +93,10 @@ class RoomListStore extends Store {
|
||||
presentationLists: defaultLists, // like `lists`, but with arrays of rooms instead
|
||||
ready: false,
|
||||
stickyRoomId: null,
|
||||
orderRoomsByImportance: true,
|
||||
};
|
||||
|
||||
SettingsStore.monitorSetting('RoomList.orderByImportance', null);
|
||||
}
|
||||
|
||||
_setState(newState) {
|
||||
@@ -99,14 +118,28 @@ class RoomListStore extends Store {
|
||||
__onDispatch(payload) {
|
||||
const logicallyReady = this._matrixClient && this._state.ready;
|
||||
switch (payload.action) {
|
||||
case 'setting_updated': {
|
||||
if (payload.settingName === 'RoomList.orderByImportance') {
|
||||
this.updateSortingAlgorithm();
|
||||
} else if (payload.settingName === 'feature_custom_tags') {
|
||||
const isActive = SettingsStore.isFeatureEnabled("feature_custom_tags");
|
||||
if (isActive !== this._state.tagsEnabled) {
|
||||
this._setState({tagsEnabled: isActive});
|
||||
this.updateSortingAlgorithm(/*force=*/true);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Initialise state after initial sync
|
||||
case 'MatrixActions.sync': {
|
||||
if (!(payload.prevState !== 'PREPARED' && payload.state === 'PREPARED')) {
|
||||
break;
|
||||
}
|
||||
|
||||
this._setState({tagsEnabled: SettingsStore.isFeatureEnabled("feature_custom_tags")});
|
||||
|
||||
this._matrixClient = payload.matrixClient;
|
||||
this._generateInitialRoomLists();
|
||||
this.updateSortingAlgorithm(/*force=*/true);
|
||||
}
|
||||
break;
|
||||
case 'MatrixActions.Room.receipt': {
|
||||
@@ -180,7 +213,7 @@ class RoomListStore extends Store {
|
||||
break;
|
||||
case 'MatrixActions.Room.myMembership': {
|
||||
if (!logicallyReady) break;
|
||||
this._roomUpdateTriggered(payload.room.roomId);
|
||||
this._roomUpdateTriggered(payload.room.roomId, true);
|
||||
}
|
||||
break;
|
||||
// This could be a new room that we've been invited to, joined or created
|
||||
@@ -188,7 +221,7 @@ class RoomListStore extends Store {
|
||||
// a member.
|
||||
case 'MatrixActions.Room': {
|
||||
if (!logicallyReady) break;
|
||||
this._roomUpdateTriggered(payload.room.roomId);
|
||||
this._roomUpdateTriggered(payload.room.roomId, true);
|
||||
}
|
||||
break;
|
||||
// TODO: Re-enable optimistic updates when we support dragging again
|
||||
@@ -230,12 +263,12 @@ class RoomListStore extends Store {
|
||||
}
|
||||
}
|
||||
|
||||
_roomUpdateTriggered(roomId) {
|
||||
_roomUpdateTriggered(roomId, ignoreSticky) {
|
||||
// We don't calculate categories for sticky rooms because we have a moderate
|
||||
// interest in trying to maintain the category that they were last in before
|
||||
// being artificially flagged as IDLE. Also, this reduces the amount of time
|
||||
// we spend in _setRoomCategory ever so slightly.
|
||||
if (this._state.stickyRoomId !== roomId) {
|
||||
if (this._state.stickyRoomId !== roomId || ignoreSticky) {
|
||||
// Micro optimization: Only look up the room if we're confident we'll need it.
|
||||
const room = this._matrixClient.getRoom(roomId);
|
||||
if (!room) return;
|
||||
@@ -245,6 +278,36 @@ class RoomListStore extends Store {
|
||||
}
|
||||
}
|
||||
|
||||
_filterTags(tags) {
|
||||
tags = tags ? Object.keys(tags) : [];
|
||||
if (this._state.tagsEnabled) return tags;
|
||||
return tags.filter((t) => !!LIST_ORDERS[t]);
|
||||
}
|
||||
|
||||
_getRecommendedTagsForRoom(room) {
|
||||
const tags = [];
|
||||
|
||||
const myMembership = room.getMyMembership();
|
||||
if (myMembership === 'join' || myMembership === 'invite') {
|
||||
// Stack the user's tags on top
|
||||
tags.push(...this._filterTags(room.tags));
|
||||
|
||||
const dmRoomMap = DMRoomMap.shared();
|
||||
if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
|
||||
tags.push("im.vector.fake.direct");
|
||||
} else if (myMembership === 'invite') {
|
||||
tags.push("im.vector.fake.invite");
|
||||
} else if (tags.length === 0) {
|
||||
tags.push("im.vector.fake.recent");
|
||||
}
|
||||
} else {
|
||||
tags.push("im.vector.fake.archived");
|
||||
}
|
||||
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
_setRoomCategory(room, category) {
|
||||
if (!room) return; // This should only happen in tests
|
||||
|
||||
@@ -260,130 +323,124 @@ class RoomListStore extends Store {
|
||||
return _targetTimestamp;
|
||||
};
|
||||
|
||||
const myMembership = room.getMyMembership();
|
||||
let doInsert = true;
|
||||
const targetTags = [];
|
||||
if (myMembership !== "join" && myMembership !== "invite") {
|
||||
doInsert = false;
|
||||
} else {
|
||||
const dmRoomMap = DMRoomMap.shared();
|
||||
if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
|
||||
targetTags.push('im.vector.fake.direct');
|
||||
} else {
|
||||
targetTags.push('im.vector.fake.recent');
|
||||
}
|
||||
}
|
||||
const targetTags = this._getRecommendedTagsForRoom(room);
|
||||
const insertedIntoTags = [];
|
||||
|
||||
// We need to update all instances of a room to ensure that they are correctly organized
|
||||
// in the list. We do this by shallow-cloning the entire `lists` object using a single
|
||||
// iterator. Within the loop, we also rebuild the list of rooms per tag (key) so that the
|
||||
// updated room gets slotted into the right spot. This sacrifices code clarity for not
|
||||
// iterating on potentially large collections multiple times.
|
||||
// We need to make sure all the tags (lists) are updated with the room's new position. We
|
||||
// generally only get called here when there's a new room to insert or a room has potentially
|
||||
// changed positions within the list.
|
||||
//
|
||||
// We do all our checks by iterating over the rooms in the existing lists, trying to insert
|
||||
// our room where we can. As a guiding principle, we should be removing the room from all
|
||||
// tags, and insert the room into targetTags. We should perform the deletion before the addition
|
||||
// where possible to keep a consistent state. By the end of this, targetTags should be the
|
||||
// same as insertedIntoTags.
|
||||
|
||||
let inserted = false;
|
||||
for (const key of Object.keys(this._state.lists)) {
|
||||
const hasRoom = this._state.lists[key].some((e) => e.room.roomId === room.roomId);
|
||||
const shouldHaveRoom = targetTags.includes(key);
|
||||
|
||||
// Speed optimization: Skip the loop below if we're not going to do anything productive
|
||||
if (!hasRoom || LIST_ORDERS[key] !== 'recent') {
|
||||
listsClone[key] = this._state.lists[key];
|
||||
if (LIST_ORDERS[key] !== 'recent' && (hasRoom || targetTags.includes(key))) {
|
||||
// Ensure that we don't try and sort the room into the tag
|
||||
inserted = true;
|
||||
doInsert = false;
|
||||
}
|
||||
continue;
|
||||
// Speed optimization: Don't do complicated math if we don't have to.
|
||||
if (!shouldHaveRoom) {
|
||||
listsClone[key] = this._state.lists[key].filter((e) => e.room.roomId !== room.roomId);
|
||||
} else if (LIST_ORDERS[key] !== 'recent') {
|
||||
// Manually ordered tags are sorted later, so for now we'll just clone the tag
|
||||
// and add our room if needed
|
||||
listsClone[key] = this._state.lists[key].filter((e) => e.room.roomId !== room.roomId);
|
||||
listsClone[key].push({room, category});
|
||||
insertedIntoTags.push(key);
|
||||
} else {
|
||||
listsClone[key] = [];
|
||||
}
|
||||
|
||||
// We track where the boundary within listsClone[key] is just in case our timestamp
|
||||
// ordering fails. If we can't stick the room in at the correct place in the category
|
||||
// grouping based on timestamp, we'll stick it at the top of the group which will be
|
||||
// the index we track here.
|
||||
let desiredCategoryBoundaryIndex = 0;
|
||||
let foundBoundary = false;
|
||||
let pushedEntry = false;
|
||||
|
||||
for (const entry of this._state.lists[key]) {
|
||||
// if the list is a recent list, and the room appears in this list, and we're not looking at a sticky
|
||||
// room (sticky rooms have unreliable categories), try to slot the new room in
|
||||
if (entry.room.roomId !== this._state.stickyRoomId) {
|
||||
if (!pushedEntry && doInsert && (targetTags.length === 0 || targetTags.includes(key))) {
|
||||
// Micro optimization: Support lazily loading the last timestamp in a room
|
||||
let _entryTimestamp = null;
|
||||
const entryTimestamp = () => {
|
||||
if (_entryTimestamp === null) {
|
||||
_entryTimestamp = this._tsOfNewestEvent(entry.room);
|
||||
}
|
||||
return _entryTimestamp;
|
||||
};
|
||||
|
||||
const entryCategoryIndex = CATEGORY_ORDER.indexOf(entry.category);
|
||||
|
||||
// As per above, check if we're meeting that boundary we wanted to locate.
|
||||
if (entryCategoryIndex >= targetCategoryIndex && !foundBoundary) {
|
||||
desiredCategoryBoundaryIndex = listsClone[key].length - 1;
|
||||
foundBoundary = true;
|
||||
}
|
||||
|
||||
// If we've hit the top of a boundary beyond our target category, insert at the top of
|
||||
// the grouping to ensure the room isn't slotted incorrectly. Otherwise, try to insert
|
||||
// based on most recent timestamp.
|
||||
const changedBoundary = entryCategoryIndex > targetCategoryIndex;
|
||||
const currentCategory = entryCategoryIndex === targetCategoryIndex;
|
||||
if (changedBoundary || (currentCategory && targetTimestamp() >= entryTimestamp())) {
|
||||
if (changedBoundary) {
|
||||
// If we changed a boundary, then we've gone too far - go to the top of the last
|
||||
// section instead.
|
||||
listsClone[key].splice(desiredCategoryBoundaryIndex, 0, {room, category});
|
||||
} else {
|
||||
// If we're ordering by timestamp, just insert normally
|
||||
listsClone[key].push({room, category});
|
||||
}
|
||||
pushedEntry = true;
|
||||
inserted = true;
|
||||
}
|
||||
}
|
||||
// We track where the boundary within listsClone[key] is just in case our timestamp
|
||||
// ordering fails. If we can't stick the room in at the correct place in the category
|
||||
// grouping based on timestamp, we'll stick it at the top of the group which will be
|
||||
// the index we track here.
|
||||
let desiredCategoryBoundaryIndex = 0;
|
||||
let foundBoundary = false;
|
||||
let pushedEntry = false;
|
||||
|
||||
for (const entry of this._state.lists[key]) {
|
||||
// We insert our own record as needed, so don't let the old one through.
|
||||
if (entry.room.roomId === room.roomId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if the list is a recent list, and the room appears in this list, and we're
|
||||
// not looking at a sticky room (sticky rooms have unreliable categories), try
|
||||
// to slot the new room in
|
||||
if (entry.room.roomId !== this._state.stickyRoomId) {
|
||||
if (!pushedEntry && shouldHaveRoom) {
|
||||
// Micro optimization: Support lazily loading the last timestamp in a room
|
||||
let _entryTimestamp = null;
|
||||
const entryTimestamp = () => {
|
||||
if (_entryTimestamp === null) {
|
||||
_entryTimestamp = this._tsOfNewestEvent(entry.room);
|
||||
}
|
||||
return _entryTimestamp;
|
||||
};
|
||||
|
||||
const entryCategoryIndex = CATEGORY_ORDER.indexOf(entry.category);
|
||||
|
||||
// As per above, check if we're meeting that boundary we wanted to locate.
|
||||
if (entryCategoryIndex >= targetCategoryIndex && !foundBoundary) {
|
||||
desiredCategoryBoundaryIndex = listsClone[key].length - 1;
|
||||
foundBoundary = true;
|
||||
}
|
||||
|
||||
// If we've hit the top of a boundary beyond our target category, insert at the top of
|
||||
// the grouping to ensure the room isn't slotted incorrectly. Otherwise, try to insert
|
||||
// based on most recent timestamp.
|
||||
const changedBoundary = entryCategoryIndex > targetCategoryIndex;
|
||||
const currentCategory = entryCategoryIndex === targetCategoryIndex;
|
||||
if (changedBoundary || (currentCategory && targetTimestamp() >= entryTimestamp())) {
|
||||
if (changedBoundary) {
|
||||
// If we changed a boundary, then we've gone too far - go to the top of the last
|
||||
// section instead.
|
||||
listsClone[key].splice(desiredCategoryBoundaryIndex, 0, {room, category});
|
||||
} else {
|
||||
// If we're ordering by timestamp, just insert normally
|
||||
listsClone[key].push({room, category});
|
||||
}
|
||||
pushedEntry = true;
|
||||
insertedIntoTags.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall through and clone the list.
|
||||
listsClone[key].push(entry);
|
||||
}
|
||||
|
||||
// Fall through and clone the list.
|
||||
listsClone[key].push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
if (!inserted) {
|
||||
// There's a good chance that we just joined the room, so we need to organize it
|
||||
// We also could have left it...
|
||||
let tags = [];
|
||||
if (doInsert) {
|
||||
tags = Object.keys(room.tags);
|
||||
if (tags.length === 0) {
|
||||
tags = targetTags;
|
||||
}
|
||||
if (tags.length === 0) {
|
||||
tags = [myMembership === 'join' ? 'im.vector.fake.recent' : 'im.vector.fake.invite'];
|
||||
}
|
||||
} else {
|
||||
tags = ['im.vector.fake.archived'];
|
||||
}
|
||||
for (const tag of tags) {
|
||||
for (let i = 0; i < listsClone[tag].length; i++) {
|
||||
// Just find the top of our category grouping and insert it there.
|
||||
const catIdxAtPosition = CATEGORY_ORDER.indexOf(listsClone[tag][i].category);
|
||||
if (catIdxAtPosition >= targetCategoryIndex) {
|
||||
listsClone[tag].splice(i, 0, {room: room, category: category});
|
||||
break;
|
||||
if (!pushedEntry) {
|
||||
if (listsClone[key].length === 0) {
|
||||
listsClone[key].push({room, category});
|
||||
insertedIntoTags.push(key);
|
||||
} else {
|
||||
// In theory, this should never happen
|
||||
console.warn(`!! Room ${room.roomId} lost: No position available`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Double check that we inserted the room in the right places
|
||||
for (const targetTag of targetTags) {
|
||||
let count = 0;
|
||||
for (const insertedTag of insertedIntoTags) {
|
||||
if (insertedTag === targetTag) count++;
|
||||
}
|
||||
|
||||
if (count !== 1) {
|
||||
console.warn(`!! Room ${room.roomId} inserted ${count} times`);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the favourites before we set the clone
|
||||
for (const tag of Object.keys(listsClone)) {
|
||||
if (LIST_ORDERS[tag] === 'recent') continue; // skip recents (pre-sorted)
|
||||
listsClone[tag].sort(this._getManualComparator(tag));
|
||||
}
|
||||
|
||||
this._setState({lists: listsClone});
|
||||
}
|
||||
|
||||
@@ -517,6 +574,14 @@ class RoomListStore extends Store {
|
||||
}
|
||||
|
||||
_calculateCategory(room) {
|
||||
if (!this._state.orderRoomsByImportance) {
|
||||
// Effectively disable the categorization of rooms if we're supposed to
|
||||
// be sorting by more recent messages first. This triggers the timestamp
|
||||
// comparison bit of _setRoomCategory and _recentsComparator instead of
|
||||
// the category ordering.
|
||||
return CATEGORY_IDLE;
|
||||
}
|
||||
|
||||
const mentions = room.getUnreadNotificationCount("highlight") > 0;
|
||||
if (mentions) return CATEGORY_RED;
|
||||
|
||||
@@ -574,7 +639,7 @@ class RoomListStore extends Store {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return a === b ? this._lexicographicalComparator(roomA, roomB) : ( a > b ? 1 : -1);
|
||||
return a === b ? this._lexicographicalComparator(roomA, roomB) : (a > b ? 1 : -1);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ class RoomViewStore extends Store {
|
||||
case 'open_room_settings': {
|
||||
const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog");
|
||||
Modal.createTrackedDialog('Room settings', '', RoomSettingsDialog, {
|
||||
roomId: this._state.roomId,
|
||||
roomId: payload.room_id || this._state.roomId,
|
||||
}, 'mx_SettingsDialog');
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user