Merge remote-tracking branch 'origin/develop' into develop
This commit is contained in:
commit
2194eebfd0
255 changed files with 3715 additions and 3159 deletions
|
@ -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>]',
|
||||
|
|
|
@ -166,6 +166,36 @@ function textForGuestAccessEvent(ev) {
|
|||
}
|
||||
}
|
||||
|
||||
function textForRelatedGroupsEvent(ev) {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
const groups = ev.getContent().groups || [];
|
||||
const prevGroups = ev.getPrevContent().groups || [];
|
||||
const added = groups.filter((g) => !prevGroups.includes(g));
|
||||
const removed = prevGroups.filter((g) => !groups.includes(g));
|
||||
|
||||
if (added.length && !removed.length) {
|
||||
return _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', {
|
||||
senderDisplayName,
|
||||
groups: added.join(', '),
|
||||
});
|
||||
} else if (!added.length && removed.length) {
|
||||
return _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', {
|
||||
senderDisplayName,
|
||||
groups: removed.join(', '),
|
||||
});
|
||||
} else if (added.length && removed.length) {
|
||||
return _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' +
|
||||
'%(oldGroups)s in this room.', {
|
||||
senderDisplayName,
|
||||
newGroups: added.join(', '),
|
||||
oldGroups: removed.join(', '),
|
||||
});
|
||||
} else {
|
||||
// Don't bother rendering this change (because there were no changes)
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function textForServerACLEvent(ev) {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
const prevContent = ev.getPrevContent();
|
||||
|
@ -473,6 +503,7 @@ const stateHandlers = {
|
|||
'm.room.tombstone': textForTombstoneEvent,
|
||||
'm.room.join_rules': textForJoinRulesEvent,
|
||||
'm.room.guest_access': textForGuestAccessEvent,
|
||||
'm.room.related_groups': textForRelatedGroupsEvent,
|
||||
|
||||
'im.vector.modular.widgets': textForWidgetEvent,
|
||||
};
|
||||
|
|
|
@ -131,6 +131,24 @@ function createRoomTagsAction(matrixClient, roomTagsEvent, room) {
|
|||
return { action: 'MatrixActions.Room.tags', room };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a MatrixActions.Room.receipt action that represents a MatrixClient
|
||||
* `Room.receipt` event, each parameter mapping to a key-value in the action.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client
|
||||
* @param {MatrixEvent} event the receipt event.
|
||||
* @param {Room} room the room the receipt happened in.
|
||||
* @returns {Object} an action of type MatrixActions.Room.receipt.
|
||||
*/
|
||||
function createRoomReceiptAction(matrixClient, event, room) {
|
||||
return {
|
||||
action: 'MatrixActions.Room.receipt',
|
||||
event,
|
||||
room,
|
||||
matrixClient,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef RoomTimelineAction
|
||||
* @type {Object}
|
||||
|
@ -233,6 +251,7 @@ export default {
|
|||
this._addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Room', createRoomAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Room.receipt', createRoomReceiptAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Room.myMembership', createSelfMembershipAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction);
|
||||
|
|
|
@ -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} />);
|
||||
|
|
|
@ -421,6 +421,7 @@ const LoggedInView = React.createClass({
|
|||
render: function() {
|
||||
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||
const RoomView = sdk.getComponent('structures.RoomView');
|
||||
const UserView = sdk.getComponent('structures.UserView');
|
||||
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
|
||||
const GroupView = sdk.getComponent('structures.GroupView');
|
||||
const MyGroups = sdk.getComponent('structures.MyGroups');
|
||||
|
@ -469,9 +470,7 @@ const LoggedInView = React.createClass({
|
|||
break;
|
||||
|
||||
case PageTypes.UserView:
|
||||
pageElement = null; // deliberately null for now
|
||||
// TODO: fix/remove UserView
|
||||
// right_panel = <RightPanel disabled={this.props.rightDisabled} />;
|
||||
pageElement = <UserView userId={this.props.currentUserId} />;
|
||||
break;
|
||||
case PageTypes.GroupView:
|
||||
pageElement = <GroupView
|
||||
|
|
|
@ -245,6 +245,17 @@ export default React.createClass({
|
|||
return this.state.defaultIsUrl || "https://vector.im";
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether to skip the server details phase of registration and start at the
|
||||
* actual form.
|
||||
* @return {boolean}
|
||||
* If there was a configured default HS or default server name, skip the
|
||||
* the server details.
|
||||
*/
|
||||
skipServerDetailsForRegistration() {
|
||||
return !!this.state.defaultHsUrl;
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
SdkConfig.put(this.props.config);
|
||||
|
||||
|
@ -1573,14 +1584,9 @@ export default React.createClass({
|
|||
this._chatCreateOrReuse(userId);
|
||||
return;
|
||||
}
|
||||
|
||||
this._setPage(PageTypes.UserView);
|
||||
this.notifyNewScreen('user/' + userId);
|
||||
const member = new Matrix.RoomMember(null, userId);
|
||||
dis.dispatch({
|
||||
action: 'view_user',
|
||||
member: member,
|
||||
});
|
||||
this.setState({currentUserId: userId});
|
||||
this._setPage(PageTypes.UserView);
|
||||
});
|
||||
} else if (screen.indexOf('group/') == 0) {
|
||||
const groupId = screen.substring(6);
|
||||
|
@ -1887,9 +1893,11 @@ export default React.createClass({
|
|||
sessionId={this.state.register_session_id}
|
||||
idSid={this.state.register_id_sid}
|
||||
email={this.props.startingFragmentQueryParams.email}
|
||||
defaultServerName={this.getDefaultServerName()}
|
||||
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
|
||||
defaultHsUrl={this.getDefaultHsUrl()}
|
||||
defaultIsUrl={this.getDefaultIsUrl()}
|
||||
skipServerDetails={this.skipServerDetailsForRegistration()}
|
||||
brand={this.props.config.brand}
|
||||
customHsUrl={this.getCurrentHsUrl()}
|
||||
customIsUrl={this.getCurrentIsUrl()}
|
||||
|
@ -1923,6 +1931,7 @@ export default React.createClass({
|
|||
<Login
|
||||
onLoggedIn={Lifecycle.setLoggedIn}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
defaultServerName={this.getDefaultServerName()}
|
||||
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
|
||||
defaultHsUrl={this.getDefaultHsUrl()}
|
||||
defaultIsUrl={this.getDefaultIsUrl()}
|
||||
|
|
|
@ -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>,
|
||||
);
|
||||
|
|
|
@ -32,6 +32,7 @@ export default class RightPanel extends React.Component {
|
|||
return {
|
||||
roomId: React.PropTypes.string, // if showing panels for a given room, this is set
|
||||
groupId: React.PropTypes.string, // if showing panels for a given group, this is set
|
||||
user: React.PropTypes.object,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -55,7 +56,7 @@ export default class RightPanel extends React.Component {
|
|||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
phase: this.props.groupId ? RightPanel.Phase.GroupMemberList : RightPanel.Phase.RoomMemberList,
|
||||
phase: this._getPhaseFromProps(),
|
||||
isUserPrivilegedInGroup: null,
|
||||
};
|
||||
this.onAction = this.onAction.bind(this);
|
||||
|
@ -69,11 +70,24 @@ export default class RightPanel extends React.Component {
|
|||
}, 500);
|
||||
}
|
||||
|
||||
_getPhaseFromProps() {
|
||||
if (this.props.groupId) {
|
||||
return RightPanel.Phase.GroupMemberList;
|
||||
} else if (this.props.user) {
|
||||
return RightPanel.Phase.RoomMemberInfo;
|
||||
} else {
|
||||
return RightPanel.Phase.RoomMemberList;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
const cli = this.context.matrixClient;
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
this._initGroupStore(this.props.groupId);
|
||||
if (this.props.user) {
|
||||
this.setState({member: this.props.user});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
|
|
@ -290,7 +290,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
return <div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src={require("../../../res/img/feather-icons/e2e/warning.svg")} width="24" height="24" title={_t("Warning")} alt="" />
|
||||
<img src={require("../../../res/img/e2e/warning.svg")} width="24" height="24" title={_t("Warning")} alt="" />
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ title }
|
||||
|
@ -309,7 +309,7 @@ module.exports = React.createClass({
|
|||
if (this._shouldShowConnectionError()) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src={require("../../../res/img/feather-icons/e2e/warning.svg")} width="24" height="24" title="/!\ " alt="/!\ " />
|
||||
<img src={require("../../../res/img/e2e/warning.svg")} width="24" height="24" title="/!\ " alt="/!\ " />
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ _t('Connectivity to the server has been lost.') }
|
||||
|
|
|
@ -282,18 +282,10 @@ const RoomSubList = React.createClass({
|
|||
this.setState({scrollTop: this.refs.scroller.getScrollTop()});
|
||||
},
|
||||
|
||||
_getRenderItems: function() {
|
||||
// try our best to not create a new array
|
||||
// because LazyRenderList rerender when the items prop
|
||||
// is not the same object as the previous value
|
||||
const {list, extraTiles} = this.props;
|
||||
if (!extraTiles || !extraTiles.length) {
|
||||
return list;
|
||||
}
|
||||
if (!list || list.length) {
|
||||
return extraTiles;
|
||||
}
|
||||
return list.concat(extraTiles);
|
||||
_canUseLazyListRendering() {
|
||||
// for now disable lazy rendering as they are already rendered tiles
|
||||
// not rooms like props.list we pass to LazyRenderList
|
||||
return !this.props.extraTiles || !this.props.extraTiles.length;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -310,7 +302,7 @@ const RoomSubList = React.createClass({
|
|||
return <div ref="subList" className={subListClasses}>
|
||||
{this._getHeaderJsx(isCollapsed)}
|
||||
</div>;
|
||||
} else {
|
||||
} else if (this._canUseLazyListRendering()) {
|
||||
return <div ref="subList" className={subListClasses}>
|
||||
{this._getHeaderJsx(isCollapsed)}
|
||||
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={ this._onScroll }>
|
||||
|
@ -319,7 +311,16 @@ const RoomSubList = React.createClass({
|
|||
height={ this.state.scrollerHeight }
|
||||
renderItem={ this.makeRoomTile }
|
||||
itemHeight={34}
|
||||
items={this._getRenderItems()} />
|
||||
items={ this.props.list } />
|
||||
</IndicatorScrollbar>
|
||||
</div>;
|
||||
} else {
|
||||
const roomTiles = this.props.list.map(r => this.makeRoomTile(r));
|
||||
const tiles = roomTiles.concat(this.props.extraTiles);
|
||||
return <div ref="subList" className={subListClasses}>
|
||||
{this._getHeaderJsx(isCollapsed)}
|
||||
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={ this._onScroll }>
|
||||
{ tiles }
|
||||
</IndicatorScrollbar>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
82
src/components/structures/UserView.js
Normal file
82
src/components/structures/UserView.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import Matrix from "matrix-js-sdk";
|
||||
import MatrixClientPeg from "../../MatrixClientPeg";
|
||||
import sdk from "../../index";
|
||||
import Modal from '../../Modal';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
export default class UserView extends React.Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
userId: React.PropTypes.string,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (this.props.userId) {
|
||||
this._loadProfileInfo();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.userId !== this.props.userId) {
|
||||
this._loadProfileInfo();
|
||||
}
|
||||
}
|
||||
|
||||
async _loadProfileInfo() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
this.setState({loading: true});
|
||||
let profileInfo;
|
||||
try {
|
||||
profileInfo = await cli.getProfileInfo(this.props.userId);
|
||||
} catch (err) {
|
||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||
Modal.createTrackedDialog(_t('Could not load user profile'), '', ErrorDialog, {
|
||||
title: _t('Could not load user profile'),
|
||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||
});
|
||||
this.setState({loading: false});
|
||||
return;
|
||||
}
|
||||
const fakeEvent = new Matrix.MatrixEvent({type: "m.room.member", content: profileInfo});
|
||||
const member = new Matrix.RoomMember(null, this.props.userId);
|
||||
member.setMembershipEvent(fakeEvent);
|
||||
this.setState({member, loading: false});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.loading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <Spinner />;
|
||||
} else if (this.state.member) {
|
||||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||
const MainSplit = sdk.getComponent('structures.MainSplit');
|
||||
const panel = <RightPanel user={this.state.member} />;
|
||||
return (<MainSplit panel={panel}><div style={{flex: "1"}} /></MainSplit>);
|
||||
} else {
|
||||
return (<div />);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -41,20 +41,22 @@ module.exports = React.createClass({
|
|||
displayName: 'ForgotPassword',
|
||||
|
||||
propTypes: {
|
||||
// The default server name to use when the user hasn't specified
|
||||
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
|
||||
// via `.well-known` discovery. The server name is used instead of the
|
||||
// HS URL when talking about "your account".
|
||||
defaultServerName: PropTypes.string,
|
||||
// An error passed along from higher up explaining that something
|
||||
// went wrong when finding the defaultHsUrl.
|
||||
defaultServerDiscoveryError: PropTypes.string,
|
||||
|
||||
defaultHsUrl: PropTypes.string,
|
||||
defaultIsUrl: PropTypes.string,
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
|
||||
onLoginClick: PropTypes.func,
|
||||
onComplete: PropTypes.func.isRequired,
|
||||
|
||||
// The default server name to use when the user hasn't specified
|
||||
// one. This is used when displaying the defaultHsUrl in the UI.
|
||||
defaultServerName: PropTypes.string,
|
||||
|
||||
// An error passed along from higher up explaining that something
|
||||
// went wrong when finding the defaultHsUrl.
|
||||
defaultServerDiscoveryError: PropTypes.string,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -234,19 +236,25 @@ module.exports = React.createClass({
|
|||
errorText = <div className="mx_Login_error">{ err }</div>;
|
||||
}
|
||||
|
||||
let yourMatrixAccountText = _t('Your account');
|
||||
try {
|
||||
const parsedHsUrl = new URL(this.state.enteredHsUrl);
|
||||
yourMatrixAccountText = _t('Your account on %(serverName)s', {
|
||||
serverName: parsedHsUrl.hostname,
|
||||
let yourMatrixAccountText = _t('Your Matrix account');
|
||||
if (this.state.enteredHsUrl === this.props.defaultHsUrl) {
|
||||
yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.defaultServerName,
|
||||
});
|
||||
} catch (e) {
|
||||
errorText = <div className="mx_Login_error">{_t(
|
||||
"The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please " +
|
||||
"enter a valid URL including the protocol prefix.",
|
||||
{
|
||||
hsUrl: this.state.enteredHsUrl,
|
||||
})}</div>;
|
||||
} else {
|
||||
try {
|
||||
const parsedHsUrl = new URL(this.state.enteredHsUrl);
|
||||
yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
|
||||
serverName: parsedHsUrl.hostname,
|
||||
});
|
||||
} catch (e) {
|
||||
errorText = <div className="mx_Login_error">{_t(
|
||||
"The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please " +
|
||||
"enter a valid URL including the protocol prefix.",
|
||||
{
|
||||
hsUrl: this.state.enteredHsUrl,
|
||||
})}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
// If custom URLs are allowed, wire up the server details edit link.
|
||||
|
|
|
@ -56,6 +56,15 @@ module.exports = React.createClass({
|
|||
|
||||
enableGuest: PropTypes.bool,
|
||||
|
||||
// The default server name to use when the user hasn't specified
|
||||
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
|
||||
// via `.well-known` discovery. The server name is used instead of the
|
||||
// HS URL when talking about where to "sign in to".
|
||||
defaultServerName: PropTypes.string,
|
||||
// An error passed along from higher up explaining that something
|
||||
// went wrong when finding the defaultHsUrl.
|
||||
defaultServerDiscoveryError: PropTypes.string,
|
||||
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
defaultHsUrl: PropTypes.string,
|
||||
|
@ -65,10 +74,6 @@ module.exports = React.createClass({
|
|||
// different homeserver without confusing users.
|
||||
fallbackHsUrl: PropTypes.string,
|
||||
|
||||
// An error passed along from higher up explaining that something
|
||||
// went wrong when finding the defaultHsUrl.
|
||||
defaultServerDiscoveryError: PropTypes.string,
|
||||
|
||||
defaultDeviceDisplayName: PropTypes.string,
|
||||
|
||||
// login shouldn't know or care how registration is done.
|
||||
|
@ -563,11 +568,20 @@ module.exports = React.createClass({
|
|||
|
||||
_renderPasswordStep: function() {
|
||||
const PasswordLogin = sdk.getComponent('auth.PasswordLogin');
|
||||
|
||||
let onEditServerDetailsClick = null;
|
||||
// If custom URLs are allowed, wire up the server details edit link.
|
||||
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
|
||||
onEditServerDetailsClick = this.onEditServerDetailsClick;
|
||||
}
|
||||
|
||||
// If the current HS URL is the default HS URL, then we can label it
|
||||
// with the default HS name (if it exists).
|
||||
let hsName;
|
||||
if (this.state.enteredHsUrl === this.props.defaultHsUrl) {
|
||||
hsName = this.props.defaultServerName;
|
||||
}
|
||||
|
||||
return (
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
|
@ -583,6 +597,7 @@ module.exports = React.createClass({
|
|||
onPhoneNumberBlur={this.onPhoneNumberBlur}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||
loginIncorrect={this.state.loginIncorrect}
|
||||
hsName={hsName}
|
||||
hsUrl={this.state.enteredHsUrl}
|
||||
disableSubmit={this.state.findingHomeserver}
|
||||
/>
|
||||
|
|
|
@ -48,31 +48,46 @@ module.exports = React.createClass({
|
|||
sessionId: PropTypes.string,
|
||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||
idSid: PropTypes.string,
|
||||
// The default server name to use when the user hasn't specified
|
||||
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
|
||||
// via `.well-known` discovery. The server name is used instead of the
|
||||
// HS URL when talking about "your account".
|
||||
defaultServerName: PropTypes.string,
|
||||
// An error passed along from higher up explaining that something
|
||||
// went wrong when finding the defaultHsUrl.
|
||||
defaultServerDiscoveryError: PropTypes.string,
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
defaultHsUrl: PropTypes.string,
|
||||
defaultIsUrl: PropTypes.string,
|
||||
skipServerDetails: PropTypes.bool,
|
||||
brand: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
// An error passed along from higher up explaining that something
|
||||
// went wrong when finding the defaultHsUrl.
|
||||
defaultServerDiscoveryError: PropTypes.string,
|
||||
// registration shouldn't know or care how login is done.
|
||||
onLoginClick: PropTypes.func.isRequired,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
const serverType = ServerType.getTypeFromHsUrl(this.props.customHsUrl);
|
||||
|
||||
const customURLsAllowed = !SdkConfig.get()['disable_custom_urls'];
|
||||
let initialPhase = PHASE_SERVER_DETAILS;
|
||||
let initialPhase = this.getDefaultPhaseForServerType(serverType);
|
||||
if (
|
||||
// if we have these two, skip to the good bit
|
||||
// (they could come in from the URL params in a
|
||||
// registration email link)
|
||||
(this.props.clientSecret && this.props.sessionId) ||
|
||||
// or if custom URLs aren't allowed, skip them
|
||||
!customURLsAllowed
|
||||
// if custom URLs aren't allowed, skip to form
|
||||
!customURLsAllowed ||
|
||||
// if other logic says to, skip to form
|
||||
this.props.skipServerDetails
|
||||
) {
|
||||
// TODO: It would seem we've now added enough conditions here that the initial
|
||||
// phase will _always_ be the form. It's tempting to remove the complexity and
|
||||
// just do that, but we keep tweaking and changing auth, so let's wait until
|
||||
// things settle a bit.
|
||||
// Filed https://github.com/vector-im/riot-web/issues/8886 to track this.
|
||||
initialPhase = PHASE_REGISTRATION;
|
||||
}
|
||||
|
||||
|
@ -94,7 +109,7 @@ module.exports = React.createClass({
|
|||
// If we've been given a session ID, we're resuming
|
||||
// straight back into UI auth
|
||||
doingUIAuth: Boolean(this.props.sessionId),
|
||||
serverType: null,
|
||||
serverType,
|
||||
hsUrl: this.props.customHsUrl,
|
||||
isUrl: this.props.customIsUrl,
|
||||
// Phase of the overall registration dialog.
|
||||
|
@ -122,7 +137,20 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onServerTypeChange(type, initial) {
|
||||
getDefaultPhaseForServerType(type) {
|
||||
switch (type) {
|
||||
case ServerType.FREE: {
|
||||
// Move directly to the registration phase since the server
|
||||
// details are fixed.
|
||||
return PHASE_REGISTRATION;
|
||||
}
|
||||
case ServerType.PREMIUM:
|
||||
case ServerType.ADVANCED:
|
||||
return PHASE_SERVER_DETAILS;
|
||||
}
|
||||
},
|
||||
|
||||
onServerTypeChange(type) {
|
||||
this.setState({
|
||||
serverType: type,
|
||||
});
|
||||
|
@ -136,10 +164,6 @@ module.exports = React.createClass({
|
|||
hsUrl,
|
||||
isUrl,
|
||||
});
|
||||
// Move directly to the registration phase since the server details are fixed.
|
||||
this.setState({
|
||||
phase: PHASE_REGISTRATION,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ServerType.PREMIUM:
|
||||
|
@ -148,17 +172,13 @@ module.exports = React.createClass({
|
|||
hsUrl: this.props.defaultHsUrl,
|
||||
isUrl: this.props.defaultIsUrl,
|
||||
});
|
||||
// if this is the initial value from the control and we're
|
||||
// already in the registration phase, don't go back to the
|
||||
// server details phase (but do if it's actually a change resulting
|
||||
// from user interaction).
|
||||
if (!initial || !this.state.phase === PHASE_REGISTRATION) {
|
||||
this.setState({
|
||||
phase: PHASE_SERVER_DETAILS,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Reset the phase to default phase for the server type.
|
||||
this.setState({
|
||||
phase: this.getDefaultPhaseForServerType(type),
|
||||
});
|
||||
},
|
||||
|
||||
_replaceClient: async function() {
|
||||
|
@ -288,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":
|
||||
|
@ -313,7 +345,7 @@ module.exports = React.createClass({
|
|||
errMsg = _t('A phone number is required to register on this homeserver.');
|
||||
break;
|
||||
case "RegistrationForm.ERR_USERNAME_INVALID":
|
||||
errMsg = _t("Only use lower case letters, numbers and '=_-./'");
|
||||
errMsg = _t("A username can only contain lower case letters, numbers and '=_-./'");
|
||||
break;
|
||||
case "RegistrationForm.ERR_USERNAME_BLANK":
|
||||
errMsg = _t('You need to enter a username.');
|
||||
|
@ -389,12 +421,9 @@ module.exports = React.createClass({
|
|||
// If we're on a different phase, we only show the server type selector,
|
||||
// which is always shown if we allow custom URLs at all.
|
||||
if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS) {
|
||||
// if we've been given a custom HS URL we should actually pass that, so
|
||||
// that the appropriate section is selected at the start to match the
|
||||
// homeserver URL we're using
|
||||
return <div>
|
||||
<ServerTypeSelector
|
||||
defaultHsUrl={this.props.customHsUrl || this.props.defaultHsUrl}
|
||||
selected={this.state.serverType}
|
||||
onChange={this.onServerTypeChange}
|
||||
/>
|
||||
</div>;
|
||||
|
@ -436,7 +465,7 @@ module.exports = React.createClass({
|
|||
|
||||
return <div>
|
||||
<ServerTypeSelector
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
selected={this.state.serverType}
|
||||
onChange={this.onServerTypeChange}
|
||||
/>
|
||||
{serverDetails}
|
||||
|
@ -466,7 +495,9 @@ module.exports = React.createClass({
|
|||
poll={true}
|
||||
/>;
|
||||
} else if (this.state.busy || !this.state.flows) {
|
||||
return <Spinner />;
|
||||
return <div className="mx_AuthBody_spinner">
|
||||
<Spinner />
|
||||
</div>;
|
||||
} else {
|
||||
let onEditServerDetailsClick = null;
|
||||
// If custom URLs are allowed and we haven't selected the Free server type, wire
|
||||
|
@ -478,6 +509,14 @@ module.exports = React.createClass({
|
|||
) {
|
||||
onEditServerDetailsClick = this.onEditServerDetailsClick;
|
||||
}
|
||||
|
||||
// If the current HS URL is the default HS URL, then we can label it
|
||||
// with the default HS name (if it exists).
|
||||
let hsName;
|
||||
if (this.state.hsUrl === this.props.defaultHsUrl) {
|
||||
hsName = this.props.defaultServerName;
|
||||
}
|
||||
|
||||
return <RegistrationForm
|
||||
defaultUsername={this.state.formVals.username}
|
||||
defaultEmail={this.state.formVals.email}
|
||||
|
@ -485,10 +524,11 @@ 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}
|
||||
hsName={hsName}
|
||||
hsUrl={this.state.hsUrl}
|
||||
/>;
|
||||
}
|
||||
|
@ -505,14 +545,9 @@ module.exports = React.createClass({
|
|||
errorText = <div className="mx_Login_error">{ err }</div>;
|
||||
}
|
||||
|
||||
let signIn;
|
||||
if (!this.state.doingUIAuth) {
|
||||
signIn = (
|
||||
<a className="mx_AuthBody_changeFlow" onClick={this.onLoginClick} href="#">
|
||||
{ _t('Sign in instead') }
|
||||
</a>
|
||||
);
|
||||
}
|
||||
const signIn = <a className="mx_AuthBody_changeFlow" onClick={this.onLoginClick} href="#">
|
||||
{ _t('Sign in instead') }
|
||||
</a>;
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
|
@ -61,29 +60,15 @@ module.exports = React.createClass({
|
|||
} else {
|
||||
console.log("Loading recaptcha script...");
|
||||
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
|
||||
const protocol = global.location.protocol;
|
||||
let protocol = global.location.protocol;
|
||||
if (protocol === "vector:") {
|
||||
const warning = document.createElement('div');
|
||||
// XXX: fix hardcoded app URL. Better solutions include:
|
||||
// * jumping straight to a hosted captcha page (but we don't support that yet)
|
||||
// * embedding the captcha in an iframe (if that works)
|
||||
// * using a better captcha lib
|
||||
ReactDOM.render(_t(
|
||||
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
|
||||
{},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a target="_blank" rel="noopener" href='https://riot.im/app'>{ sub }</a>;
|
||||
},
|
||||
}), warning);
|
||||
this.refs.recaptchaContainer.appendChild(warning);
|
||||
} else {
|
||||
const scriptTag = document.createElement('script');
|
||||
scriptTag.setAttribute(
|
||||
'src', protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit",
|
||||
);
|
||||
this.refs.recaptchaContainer.appendChild(scriptTag);
|
||||
protocol = "https:";
|
||||
}
|
||||
const scriptTag = document.createElement('script');
|
||||
scriptTag.setAttribute(
|
||||
'src', `${protocol}//www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
|
||||
);
|
||||
this.refs.recaptchaContainer.appendChild(scriptTag);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -141,8 +126,9 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div ref="recaptchaContainer">
|
||||
{ _t("This homeserver would like to make sure you are not a robot.") }
|
||||
<br />
|
||||
<p>{_t(
|
||||
"This homeserver would like to make sure you are not a robot.",
|
||||
)}</p>
|
||||
<div id={DIV_ID}></div>
|
||||
{ error }
|
||||
</div>
|
||||
|
|
|
@ -40,6 +40,10 @@ class PasswordLogin extends React.Component {
|
|||
initialPhoneNumber: "",
|
||||
initialPassword: "",
|
||||
loginIncorrect: false,
|
||||
// This is optional and only set if we used a server name to determine
|
||||
// the HS URL via `.well-known` discovery. The server name is used
|
||||
// instead of the HS URL when talking about where to "sign in to".
|
||||
hsName: null,
|
||||
hsUrl: "",
|
||||
disableSubmit: false,
|
||||
}
|
||||
|
@ -54,6 +58,7 @@ class PasswordLogin extends React.Component {
|
|||
loginType: PasswordLogin.LOGIN_FIELD_MXID,
|
||||
};
|
||||
|
||||
this.onForgotPasswordClick = this.onForgotPasswordClick.bind(this);
|
||||
this.onSubmitForm = this.onSubmitForm.bind(this);
|
||||
this.onUsernameChanged = this.onUsernameChanged.bind(this);
|
||||
this.onUsernameBlur = this.onUsernameBlur.bind(this);
|
||||
|
@ -70,6 +75,12 @@ class PasswordLogin extends React.Component {
|
|||
this._loginField = null;
|
||||
}
|
||||
|
||||
onForgotPasswordClick(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onForgotPasswordClick();
|
||||
}
|
||||
|
||||
onSubmitForm(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
|
@ -240,7 +251,7 @@ class PasswordLogin extends React.Component {
|
|||
forgotPasswordJsx = <span>
|
||||
{_t('Not sure of your password? <a>Set a new one</a>', {}, {
|
||||
a: sub => <a className="mx_Login_forgot"
|
||||
onClick={this.props.onForgotPasswordClick}
|
||||
onClick={this.onForgotPasswordClick}
|
||||
href="#"
|
||||
>
|
||||
{sub}
|
||||
|
@ -249,14 +260,20 @@ class PasswordLogin extends React.Component {
|
|||
</span>;
|
||||
}
|
||||
|
||||
let signInToText = _t('Sign in');
|
||||
try {
|
||||
const parsedHsUrl = new URL(this.props.hsUrl);
|
||||
signInToText = _t('Sign in to %(serverName)s', {
|
||||
serverName: parsedHsUrl.hostname,
|
||||
let signInToText = _t('Sign in to your Matrix account');
|
||||
if (this.props.hsName) {
|
||||
signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.hsName,
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} else {
|
||||
try {
|
||||
const parsedHsUrl = new URL(this.props.hsUrl);
|
||||
signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
|
||||
serverName: parsedHsUrl.hostname,
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
let editLink = null;
|
||||
|
@ -338,6 +355,8 @@ PasswordLogin.propTypes = {
|
|||
onPhoneNumberChanged: PropTypes.func,
|
||||
onPasswordChanged: PropTypes.func,
|
||||
loginIncorrect: PropTypes.bool,
|
||||
hsName: PropTypes.string,
|
||||
hsUrl: PropTypes.string,
|
||||
disableSubmit: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
|
|
@ -46,25 +46,28 @@ 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,
|
||||
// This is optional and only set if we used a server name to determine
|
||||
// the HS URL via `.well-known` discovery. The server name is used
|
||||
// instead of the HS URL when talking about "your account".
|
||||
hsName: PropTypes.string,
|
||||
hsUrl: PropTypes.string,
|
||||
},
|
||||
|
||||
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,
|
||||
};
|
||||
|
@ -77,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()) {
|
||||
|
@ -130,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;
|
||||
}
|
||||
}
|
||||
|
@ -202,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) {
|
||||
|
@ -236,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';
|
||||
}
|
||||
|
@ -295,14 +306,20 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
let yourMatrixAccountText = _t('Create your account');
|
||||
try {
|
||||
const parsedHsUrl = new URL(this.props.hsUrl);
|
||||
yourMatrixAccountText = _t('Create your %(serverName)s account', {
|
||||
serverName: parsedHsUrl.hostname,
|
||||
let yourMatrixAccountText = _t('Create your Matrix account');
|
||||
if (this.props.hsName) {
|
||||
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.hsName,
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} else {
|
||||
try {
|
||||
const parsedHsUrl = new URL(this.props.hsUrl);
|
||||
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
|
||||
serverName: parsedHsUrl.hostname,
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
let editLink = null;
|
||||
|
@ -310,7 +327,7 @@ module.exports = React.createClass({
|
|||
editLink = <a className="mx_AuthBody_editServerDetails"
|
||||
href="#" onClick={this.props.onEditServerDetailsClick}
|
||||
>
|
||||
{_t('Edit')}
|
||||
{_t('Change')}
|
||||
</a>;
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ export const TYPES = {
|
|||
FREE: {
|
||||
id: FREE,
|
||||
label: () => _t('Free'),
|
||||
logo: () => <img src={require('../../../../res/img/feather-icons/matrix-org-bw-logo.svg')} />,
|
||||
logo: () => <img src={require('../../../../res/img/matrix-org-bw-logo.svg')} />,
|
||||
description: () => _t('Join millions for free on the largest public server'),
|
||||
hsUrl: 'https://matrix.org',
|
||||
isUrl: 'https://vector.im',
|
||||
|
@ -38,7 +38,7 @@ export const TYPES = {
|
|||
PREMIUM: {
|
||||
id: PREMIUM,
|
||||
label: () => _t('Premium'),
|
||||
logo: () => <img src={require('../../../../res/img/feather-icons/modular-bw-logo.svg')} />,
|
||||
logo: () => <img src={require('../../../../res/img/modular-bw-logo.svg')} />,
|
||||
description: () => _t('Premium hosting for organisations <a>Learn more</a>', {}, {
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noopener">
|
||||
{sub}
|
||||
|
@ -49,21 +49,21 @@ export const TYPES = {
|
|||
id: ADVANCED,
|
||||
label: () => _t('Advanced'),
|
||||
logo: () => <div>
|
||||
<img src={require('../../../../res/img/feather-icons/globe.svg')} />
|
||||
<img src={require('../../../../res/img/feather-customised/globe.svg')} />
|
||||
{_t('Other')}
|
||||
</div>,
|
||||
description: () => _t('Find other public servers or use a custom server'),
|
||||
},
|
||||
};
|
||||
|
||||
function getDefaultType(defaultHsUrl) {
|
||||
if (!defaultHsUrl) {
|
||||
export function getTypeFromHsUrl(hsUrl) {
|
||||
if (!hsUrl) {
|
||||
return null;
|
||||
} else if (defaultHsUrl === TYPES.FREE.hsUrl) {
|
||||
} else if (hsUrl === TYPES.FREE.hsUrl) {
|
||||
return FREE;
|
||||
} else if (new URL(defaultHsUrl).hostname.endsWith('.modular.im')) {
|
||||
// TODO: Use a Riot config parameter to detect Modular-ness.
|
||||
// https://github.com/vector-im/riot-web/issues/8253
|
||||
} else if (new URL(hsUrl).hostname.endsWith('.modular.im')) {
|
||||
// This is an unlikely case to reach, as Modular defaults to hiding the
|
||||
// server type selector.
|
||||
return PREMIUM;
|
||||
} else {
|
||||
return ADVANCED;
|
||||
|
@ -72,8 +72,8 @@ function getDefaultType(defaultHsUrl) {
|
|||
|
||||
export default class ServerTypeSelector extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The default HS URL as another way to set the initially selected type.
|
||||
defaultHsUrl: PropTypes.string,
|
||||
// The default selected type.
|
||||
selected: PropTypes.string,
|
||||
// Handler called when the selected type changes.
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
@ -82,20 +82,12 @@ export default class ServerTypeSelector extends React.PureComponent {
|
|||
super(props);
|
||||
|
||||
const {
|
||||
defaultHsUrl,
|
||||
onChange,
|
||||
selected,
|
||||
} = props;
|
||||
const type = getDefaultType(defaultHsUrl);
|
||||
|
||||
this.state = {
|
||||
selected: type,
|
||||
selected,
|
||||
};
|
||||
if (onChange) {
|
||||
// FIXME: Supply a second 'initial' param here to flag that this is
|
||||
// initialising the value rather than from user interaction
|
||||
// (which sometimes we'll want to ignore). Must be a better way
|
||||
// to do this.
|
||||
onChange(type, true);
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedType(type) {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -51,7 +51,7 @@ export default class ChangelogDialog extends React.Component {
|
|||
return (
|
||||
<li key={commit.sha} className="mx_ChangelogDialog_li">
|
||||
<a href={commit.html_url} target="_blank" rel="noopener">
|
||||
{commit.commit.message}
|
||||
{commit.commit.message.split('\n')[0]}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -34,13 +34,15 @@ export default class LogoutDialog extends React.Component {
|
|||
this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this);
|
||||
this._onLogoutConfirm = this._onLogoutConfirm.bind(this);
|
||||
|
||||
const shouldLoadBackupStatus = !MatrixClientPeg.get().getKeyBackupEnabled();
|
||||
|
||||
this.state = {
|
||||
loading: false,
|
||||
loading: shouldLoadBackupStatus,
|
||||
backupInfo: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||
if (shouldLoadBackupStatus) {
|
||||
this._loadBackupStatus();
|
||||
}
|
||||
}
|
||||
|
@ -84,9 +86,17 @@ export default class LogoutDialog extends React.Component {
|
|||
}
|
||||
|
||||
_onSetRecoveryMethodClick() {
|
||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||
import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
||||
);
|
||||
if (this.state.backupInfo) {
|
||||
// A key backup exists for this account, but the creating device is not
|
||||
// verified, so restore the backup which will give us the keys from it and
|
||||
// allow us to trust it (ie. upload keys to it)
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {});
|
||||
} else {
|
||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
|
||||
);
|
||||
}
|
||||
|
||||
// close dialog
|
||||
this.props.onFinished();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -115,7 +115,7 @@ export default React.createClass({
|
|||
// user ID roughly looks okay from a Matrix perspective.
|
||||
if (!SAFE_LOCALPART_REGEX.test(this.state.username)) {
|
||||
this.setState({
|
||||
usernameError: _t("Only use lower case letters, numbers and '=_-./'"),
|
||||
usernameError: _t("A username can only contain lower case letters, numbers and '=_-./'"),
|
||||
});
|
||||
return Promise.reject();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -131,8 +131,8 @@ export default class NetworkDropdown extends React.Component {
|
|||
const options = [];
|
||||
|
||||
let servers = [];
|
||||
if (this.props.config.servers) {
|
||||
servers = servers.concat(this.props.config.servers);
|
||||
if (this.props.config.roomDirectory.servers) {
|
||||
servers = servers.concat(this.props.config.roomDirectory.servers);
|
||||
}
|
||||
|
||||
if (servers.indexOf(MatrixClientPeg.getHomeServerName()) == -1) {
|
||||
|
|
|
@ -47,7 +47,7 @@ export default class AppPermission extends React.Component {
|
|||
return (
|
||||
<div className='mx_AppPermissionWarning'>
|
||||
<div className='mx_AppPermissionWarningImage'>
|
||||
<img src={require("../../../../res/img/feather-icons/warning-triangle.svg")} alt={_t('Warning!')} />
|
||||
<img src={require("../../../../res/img/feather-customised/warning-triangle.svg")} alt={_t('Warning!')} />
|
||||
</div>
|
||||
<div className='mx_AppPermissionWarningText'>
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{ _t('Do you want to load widget from URL:') }</span> <span className='mx_AppPermissionWarningTextURL'>{ this.state.curlBase }</span>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd.
|
||||
Copyright 2017, 2019 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,142 +16,145 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import {_t} from '../../../languageHandler.js';
|
||||
import Field from "./Field";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
|
||||
const EditableItem = React.createClass({
|
||||
displayName: 'EditableItem',
|
||||
|
||||
propTypes: {
|
||||
initialValue: PropTypes.string,
|
||||
export class EditableItem extends React.Component {
|
||||
static propTypes = {
|
||||
index: PropTypes.number,
|
||||
placeholder: PropTypes.string,
|
||||
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
onRemove: PropTypes.func,
|
||||
onAdd: PropTypes.func,
|
||||
};
|
||||
|
||||
addOnChange: PropTypes.bool,
|
||||
},
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
onChange: function(value) {
|
||||
this.setState({ value });
|
||||
if (this.props.onChange) this.props.onChange(value, this.props.index);
|
||||
if (this.props.addOnChange && this.props.onAdd) this.props.onAdd(value);
|
||||
},
|
||||
this.state = {
|
||||
verifyRemove: false,
|
||||
};
|
||||
}
|
||||
|
||||
_onRemove = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({verifyRemove: true});
|
||||
};
|
||||
|
||||
_onDontRemove = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({verifyRemove: false});
|
||||
};
|
||||
|
||||
_onActuallyRemove = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
onRemove: function() {
|
||||
if (this.props.onRemove) this.props.onRemove(this.props.index);
|
||||
},
|
||||
this.setState({verifyRemove: false});
|
||||
};
|
||||
|
||||
onAdd: function() {
|
||||
if (this.props.onAdd) this.props.onAdd(this.state.value);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const EditableText = sdk.getComponent('elements.EditableText');
|
||||
return <div className="mx_EditableItem">
|
||||
<EditableText
|
||||
className="mx_EditableItem_editable"
|
||||
placeholderClassName="mx_EditableItem_editablePlaceholder"
|
||||
placeholder={this.props.placeholder}
|
||||
blurToCancel={false}
|
||||
editable={true}
|
||||
initialValue={this.props.initialValue}
|
||||
onValueChanged={this.onChange} />
|
||||
{ this.props.onAdd ?
|
||||
<div className="mx_EditableItem_addButton">
|
||||
<img className="mx_filterFlipColor"
|
||||
src={require("../../../../res/img/plus.svg")} width="14" height="14"
|
||||
alt={_t("Add")} onClick={this.onAdd} />
|
||||
render() {
|
||||
if (this.state.verifyRemove) {
|
||||
return (
|
||||
<div className="mx_EditableItem">
|
||||
<span className="mx_EditableItem_promptText">
|
||||
{_t("Are you sure?")}
|
||||
</span>
|
||||
<AccessibleButton onClick={this._onActuallyRemove} kind="primary_sm"
|
||||
className="mx_EditableItem_confirmBtn">
|
||||
{_t("Yes")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={this._onDontRemove} kind="danger_sm"
|
||||
className="mx_EditableItem_confirmBtn">
|
||||
{_t("No")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
:
|
||||
<div className="mx_EditableItem_removeButton">
|
||||
<img className="mx_filterFlipColor"
|
||||
src={require("../../../../res/img/cancel-small.svg")} width="14" height="14"
|
||||
alt={_t("Delete")} onClick={this.onRemove} />
|
||||
</div>
|
||||
}
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Make this use the new Field element
|
||||
module.exports = React.createClass({
|
||||
displayName: 'EditableItemList',
|
||||
return (
|
||||
<div className="mx_EditableItem">
|
||||
<img src={require("../../../../res/img/feather-customised/cancel.svg")} width={14} height={14}
|
||||
onClick={this._onRemove} className="mx_EditableItem_delete" alt={_t("Remove")} />
|
||||
<span className="mx_EditableItem_item">{this.props.value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
export default class EditableItemList extends React.Component {
|
||||
static propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
onNewItemChanged: PropTypes.func,
|
||||
itemsLabel: PropTypes.string,
|
||||
noItemsLabel: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
newItem: PropTypes.string,
|
||||
|
||||
onItemAdded: PropTypes.func,
|
||||
onItemEdited: PropTypes.func,
|
||||
onItemRemoved: PropTypes.func,
|
||||
onNewItemChanged: PropTypes.func,
|
||||
|
||||
canEdit: PropTypes.bool,
|
||||
},
|
||||
canRemove: PropTypes.bool,
|
||||
};
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onItemAdded: () => {},
|
||||
onItemEdited: () => {},
|
||||
onItemRemoved: () => {},
|
||||
onNewItemChanged: () => {},
|
||||
};
|
||||
},
|
||||
_onItemAdded = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
onItemAdded: function(value) {
|
||||
this.props.onItemAdded(value);
|
||||
},
|
||||
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
|
||||
};
|
||||
|
||||
onItemEdited: function(value, index) {
|
||||
if (value.length === 0) {
|
||||
this.onItemRemoved(index);
|
||||
} else {
|
||||
this.props.onItemEdited(value, index);
|
||||
}
|
||||
},
|
||||
_onItemRemoved = (index) => {
|
||||
if (this.props.onItemRemoved) this.props.onItemRemoved(index);
|
||||
};
|
||||
|
||||
onItemRemoved: function(index) {
|
||||
this.props.onItemRemoved(index);
|
||||
},
|
||||
_onNewItemChanged = (e) => {
|
||||
if (this.props.onNewItemChanged) this.props.onNewItemChanged(e.target.value);
|
||||
};
|
||||
|
||||
onNewItemChanged: function(value) {
|
||||
this.props.onNewItemChanged(value);
|
||||
},
|
||||
_renderNewItemField() {
|
||||
return (
|
||||
<form onSubmit={this._onItemAdded} autoComplete={false}
|
||||
noValidate={true} className="mx_EditableItemList_newItem">
|
||||
<Field id="newEmailAddress" label={this.props.placeholder}
|
||||
type="text" autoComplete="off" value={this.props.newItem}
|
||||
onChange={this._onNewItemChanged}
|
||||
/>
|
||||
<AccessibleButton onClick={this._onItemAdded} kind="primary">
|
||||
{_t("Add")}
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
render: function() {
|
||||
render() {
|
||||
const editableItems = this.props.items.map((item, index) => {
|
||||
if (!this.props.canRemove) {
|
||||
return <li>{item}</li>;
|
||||
}
|
||||
|
||||
return <EditableItem
|
||||
key={index}
|
||||
index={index}
|
||||
initialValue={item}
|
||||
onChange={this.onItemEdited}
|
||||
onRemove={this.onItemRemoved}
|
||||
placeholder={this.props.placeholder}
|
||||
value={item}
|
||||
onRemove={this._onItemRemoved}
|
||||
/>;
|
||||
});
|
||||
|
||||
const label = this.props.items.length > 0 ?
|
||||
this.props.itemsLabel : this.props.noItemsLabel;
|
||||
const editableItemsSection = this.props.canRemove ? editableItems : <ul>{editableItems}</ul>;
|
||||
const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel;
|
||||
|
||||
return (<div className="mx_EditableItemList">
|
||||
<div className="mx_EditableItemList_label">
|
||||
{ label }
|
||||
</div>
|
||||
{ editableItems }
|
||||
{ this.props.canEdit ?
|
||||
// This is slightly evil; we want a new instance of
|
||||
// EditableItem when the list grows. To make sure it's
|
||||
// reset to its initial state.
|
||||
<EditableItem
|
||||
key={editableItems.length}
|
||||
initialValue={this.props.newItem}
|
||||
onAdd={this.onItemAdded}
|
||||
onChange={this.onNewItemChanged}
|
||||
addOnChange={true}
|
||||
placeholder={this.props.placeholder}
|
||||
/> : <div />
|
||||
}
|
||||
{ editableItemsSection }
|
||||
{ this.props.canEdit ? this._renderNewItemField() : <div /> }
|
||||
</div>);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
});
|
||||
|
|
|
@ -150,6 +150,7 @@ export default React.createClass({
|
|||
const classes = classNames('mx_RoomTile mx_RoomTile_highlight', {
|
||||
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
|
||||
'mx_RoomTile_selected': this.state.selected,
|
||||
'mx_GroupInviteTile': true,
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
@ -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.")}
|
||||
|
|
|
@ -97,7 +97,7 @@ export default React.createClass({
|
|||
render() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const {mxEvent} = this.props;
|
||||
const colorNumber = hashCode(mxEvent.getSender()) % 8;
|
||||
const colorNumber = (hashCode(mxEvent.getSender()) % 8) + 1;
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
const {msgtype} = mxEvent.getContent();
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -15,112 +15,55 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Promise from 'bluebird';
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const ObjectUtils = require("../../../ObjectUtils");
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const sdk = require("../../../index");
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Field from "../elements/Field";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
const Modal = require("../../../Modal");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'AliasSettings',
|
||||
|
||||
propTypes: {
|
||||
export default class AliasSettings extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
canSetCanonicalAlias: PropTypes.bool.isRequired,
|
||||
canSetAliases: PropTypes.bool.isRequired,
|
||||
aliasEvents: PropTypes.array, // [MatrixEvent]
|
||||
canonicalAliasEvent: PropTypes.object, // MatrixEvent
|
||||
},
|
||||
};
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
canSetAliases: false,
|
||||
canSetCanonicalAlias: false,
|
||||
aliasEvents: [],
|
||||
};
|
||||
},
|
||||
static defaultProps = {
|
||||
canSetAliases: false,
|
||||
canSetCanonicalAlias: false,
|
||||
aliasEvents: [],
|
||||
};
|
||||
|
||||
getInitialState: function() {
|
||||
return this.recalculateState(this.props.aliasEvents, this.props.canonicalAliasEvent);
|
||||
},
|
||||
|
||||
recalculateState: function(aliasEvents, canonicalAliasEvent) {
|
||||
aliasEvents = aliasEvents || [];
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const state = {
|
||||
domainToAliases: {}, // { domain.com => [#alias1:domain.com, #alias2:domain.com] }
|
||||
remoteDomains: [], // [ domain.com, foobar.com ]
|
||||
canonicalAlias: null, // #canonical:domain.com
|
||||
updatingCanonicalAlias: false,
|
||||
newItem: "",
|
||||
};
|
||||
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
|
||||
state.domainToAliases = this.aliasEventsToDictionary(aliasEvents);
|
||||
|
||||
state.domainToAliases = this.aliasEventsToDictionary(props.aliasEvents || []);
|
||||
state.remoteDomains = Object.keys(state.domainToAliases).filter((domain) => {
|
||||
return domain !== localDomain && state.domainToAliases[domain].length > 0;
|
||||
});
|
||||
|
||||
if (canonicalAliasEvent) {
|
||||
state.canonicalAlias = canonicalAliasEvent.getContent().alias;
|
||||
if (props.canonicalAliasEvent) {
|
||||
state.canonicalAlias = props.canonicalAliasEvent.getContent().alias;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
saveSettings: function() {
|
||||
let promises = [];
|
||||
|
||||
// save new aliases for m.room.aliases
|
||||
const aliasOperations = this.getAliasOperations();
|
||||
for (let i = 0; i < aliasOperations.length; i++) {
|
||||
const alias_operation = aliasOperations[i];
|
||||
console.log("alias %s %s", alias_operation.place, alias_operation.val);
|
||||
switch (alias_operation.place) {
|
||||
case 'add':
|
||||
promises.push(
|
||||
MatrixClientPeg.get().createAlias(
|
||||
alias_operation.val, this.props.roomId,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'del':
|
||||
promises.push(
|
||||
MatrixClientPeg.get().deleteAlias(
|
||||
alias_operation.val,
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown alias operation, ignoring: " + alias_operation.place);
|
||||
}
|
||||
}
|
||||
|
||||
let oldCanonicalAlias = null;
|
||||
if (this.props.canonicalAliasEvent) {
|
||||
oldCanonicalAlias = this.props.canonicalAliasEvent.getContent().alias;
|
||||
}
|
||||
|
||||
const newCanonicalAlias = this.state.canonicalAlias;
|
||||
|
||||
if (this.props.canSetCanonicalAlias && oldCanonicalAlias !== newCanonicalAlias) {
|
||||
console.log("AliasSettings: Updating canonical alias");
|
||||
promises = [Promise.all(promises).then(
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.roomId, "m.room.canonical_alias", {
|
||||
alias: newCanonicalAlias,
|
||||
}, "",
|
||||
),
|
||||
)];
|
||||
}
|
||||
|
||||
return promises;
|
||||
},
|
||||
|
||||
aliasEventsToDictionary: function(aliasEvents) { // m.room.alias events
|
||||
aliasEventsToDictionary(aliasEvents) { // m.room.alias events
|
||||
const dict = {};
|
||||
aliasEvents.forEach((event) => {
|
||||
dict[event.getStateKey()] = (
|
||||
|
@ -128,35 +71,72 @@ module.exports = React.createClass({
|
|||
);
|
||||
});
|
||||
return dict;
|
||||
},
|
||||
}
|
||||
|
||||
isAliasValid: function(alias) {
|
||||
isAliasValid(alias) {
|
||||
// XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668
|
||||
return (alias.match(/^#([^\/:,]+?):(.+)$/) && encodeURI(alias) === alias);
|
||||
},
|
||||
return (alias.match(/^#([^/:,]+?):(.+)$/) && encodeURI(alias) === alias);
|
||||
}
|
||||
|
||||
getAliasOperations: function() {
|
||||
const oldAliases = this.aliasEventsToDictionary(this.props.aliasEvents);
|
||||
return ObjectUtils.getKeyValueArrayDiffs(oldAliases, this.state.domainToAliases);
|
||||
},
|
||||
changeCanonicalAlias(alias) {
|
||||
if (!this.props.canSetCanonicalAlias) return;
|
||||
|
||||
onNewAliasChanged: function(value) {
|
||||
this.setState({
|
||||
canonicalAlias: alias,
|
||||
updatingCanonicalAlias: true,
|
||||
});
|
||||
|
||||
const eventContent = {};
|
||||
if (alias) eventContent["alias"] = alias;
|
||||
|
||||
MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.canonical_alias",
|
||||
eventContent, "").catch((err) => {
|
||||
console.error(err);
|
||||
Modal.createTrackedDialog('Error updating main address', '', ErrorDialog, {
|
||||
title: _t("Error updating main address"),
|
||||
description: _t(
|
||||
"There was an error updating the room's main address. It may not be allowed by the server " +
|
||||
"or a temporary failure occurred.",
|
||||
),
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({updatingCanonicalAlias: false});
|
||||
});
|
||||
}
|
||||
|
||||
onNewAliasChanged = (value) => {
|
||||
this.setState({newAlias: value});
|
||||
},
|
||||
};
|
||||
|
||||
onLocalAliasAdded: function(alias) {
|
||||
onLocalAliasAdded = (alias) => {
|
||||
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
|
||||
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
if (!alias.includes(':')) alias += ':' + localDomain;
|
||||
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
|
||||
this.state.domainToAliases[localDomain] = this.state.domainToAliases[localDomain] || [];
|
||||
this.state.domainToAliases[localDomain].push(alias);
|
||||
MatrixClientPeg.get().createAlias(alias, this.props.roomId).then(() => {
|
||||
const localAliases = this.state.domainToAliases[localDomain] || [];
|
||||
const domainAliases = Object.assign({}, this.state.domainToAliases);
|
||||
domainAliases[localDomain] = [...localAliases, alias];
|
||||
|
||||
this.setState({
|
||||
domainToAliases: this.state.domainToAliases,
|
||||
// Reset the add field
|
||||
newAlias: "",
|
||||
this.setState({
|
||||
domainToAliases: domainAliases,
|
||||
// Reset the add field
|
||||
newAlias: "",
|
||||
});
|
||||
|
||||
if (!this.state.canonicalAlias) {
|
||||
this.changeCanonicalAlias(alias);
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
Modal.createTrackedDialog('Error creating alias', '', ErrorDialog, {
|
||||
title: _t("Error creating alias"),
|
||||
description: _t(
|
||||
"There was an error creating that alias. It may not be allowed by the server " +
|
||||
"or a temporary failure occurred.",
|
||||
),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
@ -165,130 +145,102 @@ module.exports = React.createClass({
|
|||
description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!this.props.canonicalAlias) {
|
||||
this.setState({
|
||||
canonicalAlias: alias,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onLocalAliasChanged: function(alias, index) {
|
||||
if (alias === "") return; // hit the delete button to delete please
|
||||
onLocalAliasDeleted = (index) => {
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
if (!alias.includes(':')) alias += ':' + localDomain;
|
||||
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
|
||||
this.state.domainToAliases[localDomain][index] = alias;
|
||||
} else {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Invalid address format', '', ErrorDialog, {
|
||||
title: _t('Invalid address format'),
|
||||
description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }),
|
||||
|
||||
const alias = this.state.domainToAliases[localDomain][index];
|
||||
|
||||
// TODO: In future, we should probably be making sure that the alias actually belongs
|
||||
// to this room. See https://github.com/vector-im/riot-web/issues/7353
|
||||
MatrixClientPeg.get().deleteAlias(alias).then(() => {
|
||||
const localAliases = this.state.domainToAliases[localDomain].filter((a) => a !== alias);
|
||||
const domainAliases = Object.assign({}, this.state.domainToAliases);
|
||||
domainAliases[localDomain] = localAliases;
|
||||
|
||||
this.setState({domainToAliases: domainAliases});
|
||||
|
||||
if (this.state.canonicalAlias === alias) {
|
||||
this.changeCanonicalAlias(null);
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
Modal.createTrackedDialog('Error removing alias', '', ErrorDialog, {
|
||||
title: _t("Error removing alias"),
|
||||
description: _t(
|
||||
"There was an error removing that alias. It may no longer exist or a temporary " +
|
||||
"error occurred.",
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onLocalAliasDeleted: function(index) {
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
// It's a bit naughty to directly manipulate this.state, and React would
|
||||
// normally whine at you, but it can't see us doing the splice. Given we
|
||||
// promptly setState anyway, it's just about acceptable. The alternative
|
||||
// would be to arbitrarily deepcopy to a temp variable and then setState
|
||||
// that, but why bother when we can cut this corner.
|
||||
const alias = this.state.domainToAliases[localDomain].splice(index, 1);
|
||||
this.setState({
|
||||
domainToAliases: this.state.domainToAliases,
|
||||
});
|
||||
if (this.props.canonicalAlias === alias) {
|
||||
this.setState({
|
||||
canonicalAlias: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
onCanonicalAliasChange: function(event) {
|
||||
this.setState({
|
||||
canonicalAlias: event.target.value,
|
||||
});
|
||||
},
|
||||
onCanonicalAliasChange = (event) => {
|
||||
this.changeCanonicalAlias(event.target.value);
|
||||
};
|
||||
|
||||
render: function() {
|
||||
const self = this;
|
||||
const EditableText = sdk.getComponent("elements.EditableText");
|
||||
render() {
|
||||
const EditableItemList = sdk.getComponent("elements.EditableItemList");
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
|
||||
let canonical_alias_section;
|
||||
if (this.props.canSetCanonicalAlias) {
|
||||
let found = false;
|
||||
const canonicalValue = this.state.canonicalAlias || "";
|
||||
canonical_alias_section = (
|
||||
<Field onChange={this.onCanonicalAliasChange} value={canonicalValue}
|
||||
element='select' id='canonicalAlias' label={_t('Main address')}>
|
||||
<option value="" key="unset">{ _t('not specified') }</option>
|
||||
{
|
||||
Object.keys(self.state.domainToAliases).map((domain, i) => {
|
||||
return self.state.domainToAliases[domain].map((alias, j) => {
|
||||
if (alias === this.state.canonicalAlias) found = true;
|
||||
return (
|
||||
<option value={alias} key={i + "_" + j}>
|
||||
{ alias }
|
||||
</option>
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
{
|
||||
found || !this.stateCanonicalAlias ? '' :
|
||||
<option value={ this.state.canonicalAlias } key='arbitrary'>
|
||||
{ this.state.canonicalAlias }
|
||||
</option>
|
||||
}
|
||||
</Field>
|
||||
);
|
||||
} else {
|
||||
canonical_alias_section = (
|
||||
<b>{ this.state.canonicalAlias || _t('not set') }</b>
|
||||
);
|
||||
}
|
||||
let found = false;
|
||||
const canonicalValue = this.state.canonicalAlias || "";
|
||||
const canonicalAliasSection = (
|
||||
<Field onChange={this.onCanonicalAliasChange} value={canonicalValue}
|
||||
disabled={this.state.updatingCanonicalAlias || !this.props.canSetCanonicalAlias}
|
||||
element='select' id='canonicalAlias' label={_t('Main address')}>
|
||||
<option value="" key="unset">{ _t('not specified') }</option>
|
||||
{
|
||||
Object.keys(this.state.domainToAliases).map((domain, i) => {
|
||||
return this.state.domainToAliases[domain].map((alias, j) => {
|
||||
if (alias === this.state.canonicalAlias) found = true;
|
||||
return (
|
||||
<option value={alias} key={i + "_" + j}>
|
||||
{ alias }
|
||||
</option>
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
{
|
||||
found || !this.state.canonicalAlias ? '' :
|
||||
<option value={ this.state.canonicalAlias } key='arbitrary'>
|
||||
{ this.state.canonicalAlias }
|
||||
</option>
|
||||
}
|
||||
</Field>
|
||||
);
|
||||
|
||||
let remote_aliases_section;
|
||||
let remoteAliasesSection;
|
||||
if (this.state.remoteDomains.length) {
|
||||
remote_aliases_section = (
|
||||
remoteAliasesSection = (
|
||||
<div>
|
||||
<div>
|
||||
{ _t("Remote addresses for this room:") }
|
||||
</div>
|
||||
<div>
|
||||
<ul>
|
||||
{ this.state.remoteDomains.map((domain, i) => {
|
||||
return this.state.domainToAliases[domain].map(function(alias, j) {
|
||||
return (
|
||||
<div key={i + "_" + j}>
|
||||
<EditableText
|
||||
className="mx_AliasSettings_alias mx_AliasSettings_editable"
|
||||
blurToCancel={false}
|
||||
editable={false}
|
||||
initialValue={alias} />
|
||||
</div>
|
||||
);
|
||||
return this.state.domainToAliases[domain].map((alias, j) => {
|
||||
return <li key={i + "_" + j}>{alias}</li>;
|
||||
});
|
||||
}) }
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mx_AliasSettings'>
|
||||
{canonical_alias_section}
|
||||
{canonicalAliasSection}
|
||||
<EditableItemList
|
||||
className={"mx_RoomSettings_localAliases"}
|
||||
items={this.state.domainToAliases[localDomain] || []}
|
||||
newItem={this.state.newAlias}
|
||||
onNewItemChanged={this.onNewAliasChanged}
|
||||
canRemove={this.props.canSetAliases}
|
||||
canEdit={this.props.canSetAliases}
|
||||
onItemAdded={this.onLocalAliasAdded}
|
||||
onItemEdited={this.onLocalAliasChanged}
|
||||
onItemRemoved={this.onLocalAliasDeleted}
|
||||
itemsLabel={_t('Local addresses for this room:')}
|
||||
noItemsLabel={_t('This room has no local addresses')}
|
||||
|
@ -296,10 +248,8 @@ module.exports = React.createClass({
|
|||
'New address (e.g. #foo:%(localDomain)s)', {localDomain: localDomain},
|
||||
)}
|
||||
/>
|
||||
|
||||
{ remote_aliases_section }
|
||||
|
||||
{remoteAliasesSection}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd.
|
||||
Copyright 2017, 2019 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -20,61 +20,50 @@ import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
|
|||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
|
||||
const GROUP_ID_REGEX = /\+\S+:\S+/;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RelatedGroupSettings',
|
||||
|
||||
propTypes: {
|
||||
export default class RelatedGroupSettings extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
canSetRelatedGroups: PropTypes.bool.isRequired,
|
||||
relatedGroupsEvent: PropTypes.instanceOf(MatrixEvent),
|
||||
},
|
||||
};
|
||||
|
||||
contextTypes: {
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
};
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
canSetRelatedGroups: false,
|
||||
static defaultProps = {
|
||||
canSetRelatedGroups: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
newGroupId: "",
|
||||
newGroupsList: props.relatedGroupsEvent ? (props.relatedGroupsEvent.getContent().groups || []) : [],
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
newGroupsList: this.getInitialGroupList(),
|
||||
newGroupId: null,
|
||||
};
|
||||
},
|
||||
updateGroups(newGroupsList) {
|
||||
this.context.matrixClient.sendStateEvent(this.props.roomId, 'm.room.related_groups', {
|
||||
groups: newGroupsList,
|
||||
}, '').catch((err) => {
|
||||
console.error(err);
|
||||
Modal.createTrackedDialog('Error updating flair', '', ErrorDialog, {
|
||||
title: _t("Error updating flair"),
|
||||
description: _t(
|
||||
"There was an error updating the flair for this room. The server may not allow it or " +
|
||||
"a temporary error occurred.",
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getInitialGroupList: function() {
|
||||
return this.props.relatedGroupsEvent ? (this.props.relatedGroupsEvent.getContent().groups || []) : [];
|
||||
},
|
||||
|
||||
needsSaving: function() {
|
||||
const cli = this.context.matrixClient;
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
if (!room.currentState.maySendStateEvent('m.room.related_groups', cli.getUserId())) return false;
|
||||
return !isEqual(this.getInitialGroupList(), this.state.newGroupsList);
|
||||
},
|
||||
|
||||
saveSettings: function() {
|
||||
if (!this.needsSaving()) return Promise.resolve();
|
||||
|
||||
return this.context.matrixClient.sendStateEvent(
|
||||
this.props.roomId,
|
||||
'm.room.related_groups',
|
||||
{
|
||||
groups: this.state.newGroupsList,
|
||||
},
|
||||
'',
|
||||
);
|
||||
},
|
||||
|
||||
validateGroupId: function(groupId) {
|
||||
validateGroupId(groupId) {
|
||||
if (!GROUP_ID_REGEX.test(groupId)) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Invalid related community ID', '', ErrorDialog, {
|
||||
|
@ -84,38 +73,32 @@ module.exports = React.createClass({
|
|||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}
|
||||
|
||||
onNewGroupChanged: function(newGroupId) {
|
||||
onNewGroupChanged = (newGroupId) => {
|
||||
this.setState({ newGroupId });
|
||||
},
|
||||
};
|
||||
|
||||
onGroupAdded: function(groupId) {
|
||||
onGroupAdded = (groupId) => {
|
||||
if (groupId.length === 0 || !this.validateGroupId(groupId)) {
|
||||
return;
|
||||
}
|
||||
const newGroupsList = [...this.state.newGroupsList, groupId];
|
||||
this.setState({
|
||||
newGroupsList: this.state.newGroupsList.concat([groupId]),
|
||||
newGroupsList: newGroupsList,
|
||||
newGroupId: '',
|
||||
});
|
||||
},
|
||||
this.updateGroups(newGroupsList);
|
||||
};
|
||||
|
||||
onGroupEdited: function(groupId, index) {
|
||||
if (groupId.length === 0 || !this.validateGroupId(groupId)) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
newGroupsList: Object.assign(this.state.newGroupsList, {[index]: groupId}),
|
||||
});
|
||||
},
|
||||
|
||||
onGroupDeleted: function(index) {
|
||||
const newGroupsList = this.state.newGroupsList.slice();
|
||||
newGroupsList.splice(index, 1);
|
||||
onGroupDeleted = (index) => {
|
||||
const group = this.state.newGroupsList[index];
|
||||
const newGroupsList = this.state.newGroupsList.filter((g) => g !== group);
|
||||
this.setState({ newGroupsList });
|
||||
},
|
||||
this.updateGroups(newGroupsList);
|
||||
};
|
||||
|
||||
render: function() {
|
||||
render() {
|
||||
const localDomain = this.context.matrixClient.getDomain();
|
||||
const EditableItemList = sdk.getComponent('elements.EditableItemList');
|
||||
return <div>
|
||||
|
@ -123,10 +106,10 @@ module.exports = React.createClass({
|
|||
items={this.state.newGroupsList}
|
||||
className={"mx_RelatedGroupSettings"}
|
||||
newItem={this.state.newGroupId}
|
||||
canRemove={this.props.canSetRelatedGroups}
|
||||
canEdit={this.props.canSetRelatedGroups}
|
||||
onNewItemChanged={this.onNewGroupChanged}
|
||||
onItemAdded={this.onGroupAdded}
|
||||
onItemEdited={this.onGroupEdited}
|
||||
onItemRemoved={this.onGroupDeleted}
|
||||
itemsLabel={_t('Showing flair for these communities:')}
|
||||
noItemsLabel={_t('This room is not showing flair for any communities')}
|
||||
|
@ -135,5 +118,5 @@ module.exports = React.createClass({
|
|||
)}
|
||||
/>
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
@ -65,6 +64,7 @@ const stateEventTileTypes = {
|
|||
'm.room.tombstone': 'messages.TextualEvent',
|
||||
'm.room.join_rules': 'messages.TextualEvent',
|
||||
'm.room.guest_access': 'messages.TextualEvent',
|
||||
'm.room.related_groups': 'messages.TextualEvent',
|
||||
};
|
||||
|
||||
function getHandlerTile(ev) {
|
||||
|
@ -320,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});
|
||||
},
|
||||
|
@ -540,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();
|
||||
|
||||
|
@ -693,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 }
|
||||
|
@ -702,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}
|
||||
|
@ -717,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">
|
||||
|
@ -728,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}
|
||||
|
|
|
@ -980,12 +980,18 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
let backButton;
|
||||
if (this.props.member.roomId) {
|
||||
backButton = (<AccessibleButton className="mx_MemberInfo_cancel"
|
||||
onClick={this.onCancel}
|
||||
title={_t('Close')}
|
||||
/>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<div className="mx_MemberInfo_name">
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}>
|
||||
<img src={require("../../../../res/img/minimise.svg")} width="10" height="16" className="mx_filterFlipColor" alt={_t('Close')} />
|
||||
</AccessibleButton>
|
||||
{ backButton }
|
||||
{ e2eIconElement }
|
||||
<EmojiText element="h2">{ memberName }</EmojiText>
|
||||
</div>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -68,6 +68,7 @@ export default class MessageComposer extends React.Component {
|
|||
showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
|
||||
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
|
||||
tombstone: this._getRoomTombstone(),
|
||||
canSendMessages: this.props.room.maySendMessage(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -120,6 +121,9 @@ export default class MessageComposer extends React.Component {
|
|||
if (ev.getType() === 'm.room.tombstone') {
|
||||
this.setState({tombstone: this._getRoomTombstone()});
|
||||
}
|
||||
if (ev.getType() === 'm.room.power_levels') {
|
||||
this.setState({canSendMessages: this.props.room.maySendMessage()});
|
||||
}
|
||||
}
|
||||
|
||||
_getRoomTombstone() {
|
||||
|
@ -257,6 +261,8 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
onInputStateChanged(inputState) {
|
||||
// Merge the new input state with old to support partial updates
|
||||
inputState = Object.assign({}, this.state.inputState, inputState);
|
||||
this.setState({inputState});
|
||||
}
|
||||
|
||||
|
@ -357,38 +363,7 @@ export default class MessageComposer extends React.Component {
|
|||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const canSendMessages = !this.state.tombstone &&
|
||||
this.props.room.maySendMessage();
|
||||
|
||||
// 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 (canSendMessages) {
|
||||
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
|
||||
// complex because of conference calls.
|
||||
|
@ -441,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,
|
||||
|
@ -467,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') }
|
||||
|
@ -499,15 +473,16 @@ export default class MessageComposer extends React.Component {
|
|||
<div className="mx_MessageComposer_formatbar_wrapper">
|
||||
<div className="mx_MessageComposer_formatbar">
|
||||
{ formatButtons }
|
||||
<div style={{flex: 1}}></div>
|
||||
<img title={this.state.inputState.isRichTextEnabled ? _t("Turn Markdown on") : _t("Turn Markdown off")}
|
||||
onMouseDown={this.onToggleMarkdownClicked}
|
||||
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
|
||||
src={require(`../../../../res/img/button-md-${!this.state.inputState.isRichTextEnabled}.png`)} />
|
||||
<div style={{ flex: 1 }}></div>
|
||||
<AccessibleButton className="mx_MessageComposer_formatbar_markdown mx_MessageComposer_markdownDisabled"
|
||||
onClick={this.onToggleMarkdownClicked}
|
||||
title={_t("Markdown is disabled")}
|
||||
/>
|
||||
<AccessibleButton element="img" title={_t("Hide Text Formatting Toolbar")}
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
|
||||
src={require("../../../../res/img/icon-text-cancel.svg")} />
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
|
||||
src={require("../../../../res/img/icon-text-cancel.svg")}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ import ReplyPreview from "./ReplyPreview";
|
|||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import {ContentHelpers} from 'matrix-js-sdk';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
||||
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
|
||||
|
@ -627,7 +628,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
const inputState = {
|
||||
marks: editorState.activeMarks,
|
||||
isRichTextEnabled: this.state.isRichTextEnabled,
|
||||
blockType,
|
||||
};
|
||||
this.props.onInputStateChanged(inputState);
|
||||
|
@ -685,20 +685,22 @@ export default class MessageComposerInput extends React.Component {
|
|||
enableRichtext(enabled: boolean) {
|
||||
if (enabled === this.state.isRichTextEnabled) return;
|
||||
|
||||
let editorState = null;
|
||||
if (enabled) {
|
||||
editorState = this.mdToRichEditorState(this.state.editorState);
|
||||
} else {
|
||||
editorState = this.richToMdEditorState(this.state.editorState);
|
||||
}
|
||||
|
||||
Analytics.setRichtextMode(enabled);
|
||||
|
||||
this.setState({
|
||||
editorState: this.createEditorState(enabled, editorState),
|
||||
editorState: this.createEditorState(
|
||||
enabled,
|
||||
this.state.editorState,
|
||||
this.state.isRichTextEnabled,
|
||||
),
|
||||
isRichTextEnabled: enabled,
|
||||
}, ()=>{
|
||||
}, () => {
|
||||
this._editor.focus();
|
||||
if (this.props.onInputStateChanged) {
|
||||
this.props.onInputStateChanged({
|
||||
isRichTextEnabled: enabled,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
SettingsStore.setValue("MessageComposerInput.isRichTextEnabled", null, SettingLevel.ACCOUNT, enabled);
|
||||
|
@ -1193,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;
|
||||
|
@ -1582,6 +1584,11 @@ export default class MessageComposerInput extends React.Component {
|
|||
placeholder = undefined;
|
||||
}
|
||||
|
||||
const markdownClasses = classNames({
|
||||
mx_MessageComposer_input_markdownIndicator: true,
|
||||
mx_MessageComposer_markdownDisabled: this.state.isRichTextEnabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx_MessageComposer_input_wrapper" onClick={this.focusComposer}>
|
||||
<div className="mx_MessageComposer_autocomplete_wrapper">
|
||||
|
@ -1596,10 +1603,10 @@ export default class MessageComposerInput extends React.Component {
|
|||
/>
|
||||
</div>
|
||||
<div className={className}>
|
||||
<img className="mx_MessageComposer_input_markdownIndicator mx_filterFlipColor"
|
||||
onMouseDown={this.onMarkdownToggleClicked}
|
||||
title={this.state.isRichTextEnabled ? _t("Markdown is disabled") : _t("Markdown is enabled")}
|
||||
src={require(`../../../../res/img/button-md-${!this.state.isRichTextEnabled}.png`)} />
|
||||
<AccessibleButton className={markdownClasses}
|
||||
onClick={this.onMarkdownToggleClicked}
|
||||
title={this.state.isRichTextEnabled ? _t("Markdown is disabled") : _t("Markdown is enabled")}
|
||||
/>
|
||||
<Editor ref={this._collectEditor}
|
||||
dir="auto"
|
||||
className="mx_MessageComposer_editor"
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -102,7 +102,7 @@ export class ExistingEmailAddress extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="mx_ExistingEmailAddress">
|
||||
<img src={require("../../../../res/img/feather-icons/cancel.svg")} width={14} height={14}
|
||||
<img src={require("../../../../res/img/feather-customised/cancel.svg")} width={14} height={14}
|
||||
onClick={this._onRemove} className="mx_ExistingEmailAddress_delete" alt={_t("Remove")} />
|
||||
<span className="mx_ExistingEmailAddress_email">{this.props.email.address}</span>
|
||||
</div>
|
||||
|
|
|
@ -28,7 +28,7 @@ import {
|
|||
PushRuleVectorState,
|
||||
ContentRules,
|
||||
} from '../../../notifications';
|
||||
import * as SdkConfig from "../../../SdkConfig";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
|
||||
// TODO: this "view" component still has far too much application logic in it,
|
||||
|
@ -132,9 +132,9 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onEnableEmailNotificationsChange: function(address, event) {
|
||||
onEnableEmailNotificationsChange: function(address, checked) {
|
||||
let emailPusherPromise;
|
||||
if (event.target.checked) {
|
||||
if (checked) {
|
||||
const data = {};
|
||||
data['brand'] = SdkConfig.get().brand || 'Riot';
|
||||
emailPusherPromise = UserSettingsStore.addEmailPusher(address, data);
|
||||
|
|
|
@ -97,7 +97,7 @@ export class ExistingPhoneNumber extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="mx_ExistingPhoneNumber">
|
||||
<img src={require("../../../../res/img/feather-icons/cancel.svg")} width={14} height={14}
|
||||
<img src={require("../../../../res/img/feather-customised/cancel.svg")} width={14} height={14}
|
||||
onClick={this._onRemove} className="mx_ExistingPhoneNumber_delete" alt={_t("Remove")} />
|
||||
<span className="mx_ExistingPhoneNumber_address">+{this.props.msisdn.address}</span>
|
||||
</div>
|
||||
|
|
|
@ -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 = {
|
||||
|
@ -68,18 +68,6 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_saveAliases = (e) => {
|
||||
// TODO: Live modification?
|
||||
if (!this.refs.aliasSettings) return;
|
||||
this.refs.aliasSettings.saveSettings();
|
||||
};
|
||||
|
||||
_saveGroups = (e) => {
|
||||
// TODO: Live modification?
|
||||
if (!this.refs.flairSettings) return;
|
||||
this.refs.flairSettings.saveSettings();
|
||||
};
|
||||
|
||||
_onLeaveClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'leave_room',
|
||||
|
@ -113,12 +101,9 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Room Addresses")}</span>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<AliasSettings ref="aliasSettings" roomId={this.props.roomId}
|
||||
<AliasSettings roomId={this.props.roomId}
|
||||
canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases}
|
||||
canonicalAliasEvent={canonicalAliasEv} aliasEvents={aliasEvents} />
|
||||
<AccessibleButton onClick={this._saveAliases} kind='primary'>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<LabelledToggleSwitch value={this.state.isRoomPublished}
|
||||
|
@ -131,12 +116,9 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Flair")}</span>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<RelatedGroupSettings ref="flairSettings" roomId={room.roomId}
|
||||
<RelatedGroupSettings roomId={room.roomId}
|
||||
canSetRelatedGroups={canChangeGroups}
|
||||
relatedGroupsEvent={groupsEvent} />
|
||||
<AccessibleButton onClick={this._saveGroups} kind='primary'>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
||||
<span className='mx_SettingsTab_subheading'>{_t("URL Previews")}</span>
|
|
@ -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.
|
||||
|
@ -116,7 +116,8 @@ export default class RolesRoomSettingsTab extends React.Component {
|
|||
_onPowerLevelsChanged = (value, powerLevelKey) => {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
let plContent = room.currentState.getStateEvents('m.room.power_levels', '').getContent() || {};
|
||||
const plEvent = room.currentState.getStateEvents('m.room.power_levels', '');
|
||||
let plContent = plEvent ? (plEvent.getContent() || {}) : {};
|
||||
|
||||
// Clone the power levels just in case
|
||||
plContent = Object.assign({}, plContent);
|
||||
|
@ -151,7 +152,8 @@ export default class RolesRoomSettingsTab extends React.Component {
|
|||
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
const plContent = room.currentState.getStateEvents('m.room.power_levels', '').getContent() || {};
|
||||
const plEvent = room.currentState.getStateEvents('m.room.power_levels', '');
|
||||
const plContent = plEvent ? (plEvent.getContent() || {}) : {};
|
||||
const canChangeLevels = room.currentState.mayClientSendStateEvent('m.room.power_levels', client);
|
||||
|
||||
const powerLevelDescriptors = {
|
|
@ -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 = {
|
||||
|
@ -43,13 +43,31 @@ export default class SecurityRoomSettingsTab extends React.Component {
|
|||
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
const state = room.currentState;
|
||||
const joinRule = state.getStateEvents("m.room.join_rules", "").getContent()['join_rule'];
|
||||
const guestAccess = state.getStateEvents("m.room.guest_access", "").getContent()['guest_access'];
|
||||
const history = state.getStateEvents("m.room.history_visibility", "").getContent()['history_visibility'];
|
||||
|
||||
const joinRule = this._pullContentPropertyFromEvent(
|
||||
state.getStateEvents("m.room.join_rules", ""),
|
||||
'join_rule',
|
||||
'invite',
|
||||
);
|
||||
const guestAccess = this._pullContentPropertyFromEvent(
|
||||
state.getStateEvents("m.room.guest_access", ""),
|
||||
'guest_access',
|
||||
'forbidden',
|
||||
);
|
||||
const history = this._pullContentPropertyFromEvent(
|
||||
state.getStateEvents("m.room.history_visibility", ""),
|
||||
'history_visibility',
|
||||
'shared',
|
||||
);
|
||||
const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
|
||||
this.setState({joinRule, guestAccess, history, encrypted});
|
||||
}
|
||||
|
||||
_pullContentPropertyFromEvent(event, key, defaultValue) {
|
||||
if (!event || !event.getContent()) return defaultValue;
|
||||
return event.getContent()[key] || defaultValue;
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent);
|
||||
}
|
||||
|
@ -170,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>
|
||||
|
@ -183,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() {
|
||||
|
@ -145,7 +145,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
|
||||
<Field id="theme" label={_t("Theme")} element="select"
|
||||
value={this.state.theme} onChange={this._onThemeChange}>
|
||||
<option value="light">{_t("Default theme")}</option>
|
||||
<option value="light">{_t("Light theme")}</option>
|
||||
<option value="dark">{_t("Dark theme")}</option>
|
||||
</Field>
|
||||
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} />
|
|
@ -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,26 +15,21 @@ 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',
|
||||
'sendTypingNotifications',
|
||||
];
|
||||
|
||||
static ROOM_LIST_SETTINGS = [
|
||||
'pinUnreadRooms',
|
||||
'pinMentionedRooms',
|
||||
];
|
||||
|
||||
static TIMELINE_SETTINGS = [
|
||||
'autoplayGifsAndVideos',
|
||||
'urlPreviewsEnabled',
|
||||
|
@ -49,6 +44,10 @@ export default class PreferencesSettingsTab extends React.Component {
|
|||
'showDisplaynameChanges',
|
||||
];
|
||||
|
||||
static ROOM_LIST_SETTINGS = [
|
||||
'RoomList.orderByImportance',
|
||||
];
|
||||
|
||||
static ADVANCED_SETTINGS = [
|
||||
'alwaysShowEncryptionIcons',
|
||||
'Pill.shouldShowPillAvatar',
|
||||
|
@ -64,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);
|
||||
};
|
||||
|
@ -99,21 +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)}
|
||||
|
||||
<span className="mx_SettingsTab_subheading">{_t("Room list")}</span>
|
||||
{this._renderGroup(PreferencesSettingsTab.ROOM_LIST_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}
|
|
@ -140,7 +140,7 @@ _td("Light bulb");
|
|||
_td("Book");
|
||||
_td("Pencil");
|
||||
_td("Paperclip");
|
||||
_td("Scisors");
|
||||
_td("Scissors");
|
||||
_td("Padlock");
|
||||
_td("Key");
|
||||
_td("Hammer");
|
||||
|
|
|
@ -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.",
|
||||
|
@ -192,6 +193,9 @@
|
|||
"%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s has allowed guests to join the room.",
|
||||
"%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s has prevented guests from joining the room.",
|
||||
"%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s changed guest access to %(rule)s",
|
||||
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s enabled flair for %(groups)s in this room.",
|
||||
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s disabled flair for %(groups)s in this room.",
|
||||
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.",
|
||||
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.",
|
||||
"%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s added %(addedAddresses)s as addresses for this room.",
|
||||
"%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s added %(addedAddresses)s as an address for this room.",
|
||||
|
@ -279,7 +283,7 @@
|
|||
"Show join/leave messages (invites/kicks/bans unaffected)": "Show join/leave messages (invites/kicks/bans unaffected)",
|
||||
"Show avatar changes": "Show avatar changes",
|
||||
"Show display name changes": "Show display name changes",
|
||||
"Show read receipts": "Show read receipts",
|
||||
"Show read receipts sent by other users": "Show read receipts sent by other users",
|
||||
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
|
||||
"Always show message timestamps": "Always show message timestamps",
|
||||
"Autoplay GIFs and videos": "Autoplay GIFs and videos",
|
||||
|
@ -300,11 +304,10 @@
|
|||
"Enable URL previews for this room (only affects you)": "Enable URL previews for this room (only affects you)",
|
||||
"Enable URL previews by default for participants in this room": "Enable URL previews by default for participants in this room",
|
||||
"Room Colour": "Room Colour",
|
||||
"Pin rooms I'm mentioned in to the top of the room list": "Pin rooms I'm mentioned in to the top of the room list",
|
||||
"Pin unread rooms to the top of the room list": "Pin unread rooms to the top of the room list",
|
||||
"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",
|
||||
|
@ -382,7 +385,7 @@
|
|||
"Book": "Book",
|
||||
"Pencil": "Pencil",
|
||||
"Paperclip": "Paperclip",
|
||||
"Scisors": "Scisors",
|
||||
"Scissors": "Scissors",
|
||||
"Padlock": "Padlock",
|
||||
"Key": "Key",
|
||||
"Hammer": "Hammer",
|
||||
|
@ -499,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",
|
||||
|
@ -522,11 +513,12 @@
|
|||
"Phone numbers": "Phone numbers",
|
||||
"Language and region": "Language and region",
|
||||
"Theme": "Theme",
|
||||
"Default theme": "Default theme",
|
||||
"Light theme": "Light theme",
|
||||
"Dark theme": "Dark theme",
|
||||
"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>.",
|
||||
|
@ -550,11 +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",
|
||||
"Room list": "Room list",
|
||||
"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",
|
||||
|
@ -714,15 +718,13 @@
|
|||
"This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",
|
||||
"The conversation continues here.": "The conversation continues here.",
|
||||
"You do not have permission to post to this room": "You do not have permission to post to this room",
|
||||
"Turn Markdown on": "Turn Markdown on",
|
||||
"Turn Markdown off": "Turn Markdown off",
|
||||
"Markdown is disabled": "Markdown is disabled",
|
||||
"Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar",
|
||||
"Server error": "Server error",
|
||||
"Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",
|
||||
"Command error": "Command error",
|
||||
"Unable to reply": "Unable to reply",
|
||||
"At this time it is not possible to reply with an emote.": "At this time it is not possible to reply with an emote.",
|
||||
"Markdown is disabled": "Markdown is disabled",
|
||||
"Markdown is enabled": "Markdown is enabled",
|
||||
"No pinned messages.": "No pinned messages.",
|
||||
"Loading...": "Loading...",
|
||||
|
@ -809,17 +811,22 @@
|
|||
"Hide Stickers": "Hide Stickers",
|
||||
"Show Stickers": "Show Stickers",
|
||||
"Jump to first unread message.": "Jump to first unread message.",
|
||||
"Error updating main address": "Error updating main address",
|
||||
"There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.",
|
||||
"Error creating alias": "Error creating alias",
|
||||
"There was an error creating that alias. It may not be allowed by the server or a temporary failure occurred.": "There was an error creating that alias. It may not be allowed by the server or a temporary failure occurred.",
|
||||
"Invalid alias format": "Invalid alias format",
|
||||
"'%(alias)s' is not a valid format for an alias": "'%(alias)s' is not a valid format for an alias",
|
||||
"Invalid address format": "Invalid address format",
|
||||
"'%(alias)s' is not a valid format for an address": "'%(alias)s' is not a valid format for an address",
|
||||
"Error removing alias": "Error removing alias",
|
||||
"There was an error removing that alias. It may no longer exist or a temporary error occurred.": "There was an error removing that alias. It may no longer exist or a temporary error occurred.",
|
||||
"Main address": "Main address",
|
||||
"not specified": "not specified",
|
||||
"not set": "not set",
|
||||
"Remote addresses for this room:": "Remote addresses for this room:",
|
||||
"Local addresses for this room:": "Local addresses for this room:",
|
||||
"This room has no local addresses": "This room has no local addresses",
|
||||
"New address (e.g. #foo:%(localDomain)s)": "New address (e.g. #foo:%(localDomain)s)",
|
||||
"Error updating flair": "Error updating flair",
|
||||
"There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.",
|
||||
"Invalid community ID": "Invalid community ID",
|
||||
"'%(groupId)s' is not a valid community ID": "'%(groupId)s' is not a valid community ID",
|
||||
"Showing flair for these communities:": "Showing flair for these communities:",
|
||||
|
@ -930,7 +937,6 @@
|
|||
"Verify...": "Verify...",
|
||||
"Join": "Join",
|
||||
"No results": "No results",
|
||||
"Delete": "Delete",
|
||||
"Communities": "Communities",
|
||||
"Home": "Home",
|
||||
"You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)",
|
||||
|
@ -1111,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",
|
||||
|
@ -1134,7 +1140,7 @@
|
|||
"Email address": "Email address",
|
||||
"This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.",
|
||||
"Skip": "Skip",
|
||||
"Only use lower case letters, numbers and '=_-./'": "Only use lower case letters, numbers and '=_-./'",
|
||||
"A username can only contain lower case letters, numbers and '=_-./'": "A username can only contain lower case letters, numbers and '=_-./'",
|
||||
"Username not available": "Username not available",
|
||||
"Username invalid: %(errMessage)s": "Username invalid: %(errMessage)s",
|
||||
"An error occurred: %(error_string)s": "An error occurred: %(error_string)s",
|
||||
|
@ -1199,6 +1205,7 @@
|
|||
"View Source": "View Source",
|
||||
"View Decrypted Source": "View Decrypted Source",
|
||||
"Unhide Preview": "Unhide Preview",
|
||||
"Share Permalink": "Share Permalink",
|
||||
"Share Message": "Share Message",
|
||||
"Quote": "Quote",
|
||||
"Source URL": "Source URL",
|
||||
|
@ -1224,7 +1231,6 @@
|
|||
"Sign in": "Sign in",
|
||||
"Login": "Login",
|
||||
"powered by Matrix": "powered by Matrix",
|
||||
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>": "Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
|
||||
"This homeserver would like to make sure you are not a robot.": "This homeserver would like to make sure you are not a robot.",
|
||||
"Custom Server Options": "Custom Server Options",
|
||||
"You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use this app with an existing Matrix account on a different homeserver.": "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use this app with an existing Matrix account on a different homeserver.",
|
||||
|
@ -1252,13 +1258,14 @@
|
|||
"Username": "Username",
|
||||
"Mobile phone number": "Mobile phone number",
|
||||
"Not sure of your password? <a>Set a new one</a>": "Not sure of your password? <a>Set a new one</a>",
|
||||
"Sign in to %(serverName)s": "Sign in to %(serverName)s",
|
||||
"Sign in to your Matrix account": "Sign in to your Matrix account",
|
||||
"Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s",
|
||||
"Change": "Change",
|
||||
"Sign in with": "Sign in with",
|
||||
"Phone": "Phone",
|
||||
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
|
||||
"Create your account": "Create your account",
|
||||
"Create your %(serverName)s account": "Create your %(serverName)s account",
|
||||
"Create your Matrix account": "Create your Matrix account",
|
||||
"Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s",
|
||||
"Email": "Email",
|
||||
"Email (optional)": "Email (optional)",
|
||||
"Phone (optional)": "Phone (optional)",
|
||||
|
@ -1325,6 +1332,7 @@
|
|||
"Community %(groupId)s not found": "Community %(groupId)s not found",
|
||||
"This homeserver does not support communities": "This homeserver does not support communities",
|
||||
"Failed to load %(groupId)s": "Failed to load %(groupId)s",
|
||||
"Filter room names": "Filter room names",
|
||||
"Invalid configuration: Cannot supply a default homeserver URL and a default server name": "Invalid configuration: Cannot supply a default homeserver URL and a default server name",
|
||||
"Failed to reject invitation": "Failed to reject invitation",
|
||||
"This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.",
|
||||
|
@ -1394,7 +1402,6 @@
|
|||
"Click to mute video": "Click to mute video",
|
||||
"Click to unmute audio": "Click to unmute audio",
|
||||
"Click to mute audio": "Click to mute audio",
|
||||
"Filter room names": "Filter room names",
|
||||
"Clear filter": "Clear filter",
|
||||
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
|
||||
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
|
||||
|
@ -1403,13 +1410,14 @@
|
|||
"Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others",
|
||||
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
|
||||
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
|
||||
"Could not load user profile": "Could not load user profile",
|
||||
"Failed to send email": "Failed to send email",
|
||||
"The email address linked to your account must be entered.": "The email address linked to your account must be entered.",
|
||||
"A new password must be entered.": "A new password must be entered.",
|
||||
"New passwords must match each other.": "New passwords must match each other.",
|
||||
"Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.",
|
||||
"Your account": "Your account",
|
||||
"Your account on %(serverName)s": "Your account on %(serverName)s",
|
||||
"Your Matrix account": "Your Matrix account",
|
||||
"Your Matrix account on %(serverName)s": "Your Matrix account on %(serverName)s",
|
||||
"The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please enter a valid URL including the protocol prefix.": "The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please enter a valid URL including the protocol prefix.",
|
||||
"A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.",
|
||||
"Send Reset Email": "Send Reset Email",
|
||||
|
@ -1453,6 +1461,7 @@
|
|||
"A phone number is required to register on this homeserver.": "A phone number is required to register on this homeserver.",
|
||||
"You need to enter a username.": "You need to enter a username.",
|
||||
"An unknown error occurred.": "An unknown error occurred.",
|
||||
"Create your account": "Create your account",
|
||||
"Commands": "Commands",
|
||||
"Results from DuckDuckGo": "Results from DuckDuckGo",
|
||||
"Emoji": "Emoji",
|
||||
|
|
|
@ -338,8 +338,10 @@ export function getCurrentLanguage() {
|
|||
|
||||
function getLangsJson() {
|
||||
return new Promise((resolve, reject) => {
|
||||
// LANGUAGES_FILE is a webpack compile-time define, see webpack config
|
||||
const url = (typeof LANGUAGES_FILE === "string") ? require(LANGUAGES_FILE) : (i18nFolder + 'languages.json');
|
||||
request(
|
||||
{ method: "GET", url: i18nFolder + 'languages.json' },
|
||||
{ method: "GET", url },
|
||||
(err, response, body) => {
|
||||
if (err || response.status < 200 || response.status >= 300) {
|
||||
reject({err: err, response: response});
|
||||
|
|
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.
|
||||
|
@ -155,7 +155,7 @@ export const SETTINGS = {
|
|||
},
|
||||
"showReadReceipts": {
|
||||
supportedLevels: LEVELS_ROOM_SETTINGS,
|
||||
displayName: _td('Show read receipts'),
|
||||
displayName: _td('Show read receipts sent by other users'),
|
||||
default: true,
|
||||
invertedSettingName: 'hideReadReceipts',
|
||||
},
|
||||
|
@ -321,16 +321,6 @@ export const SETTINGS = {
|
|||
default: true,
|
||||
controller: new AudioNotificationsEnabledController(),
|
||||
},
|
||||
"pinMentionedRooms": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td("Pin rooms I'm mentioned in to the top of the room list"),
|
||||
default: true,
|
||||
},
|
||||
"pinUnreadRooms": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td("Pin unread rooms to the top of the room list"),
|
||||
default: true,
|
||||
},
|
||||
"enableWidgetScreenshots": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Enable widget screenshots on supported widgets'),
|
||||
|
@ -350,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,8 +24,10 @@ 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";
|
||||
import {WatchManager} from "./WatchManager";
|
||||
|
||||
/**
|
||||
* Represents the various setting levels supported by the SettingsStore.
|
||||
|
@ -41,6 +44,8 @@ export const SettingLevel = {
|
|||
DEFAULT: "default",
|
||||
};
|
||||
|
||||
const defaultWatchManager = new WatchManager();
|
||||
|
||||
// Convert the settings to easier to manage objects for the handlers
|
||||
const defaultSettings = {};
|
||||
const invertedDefaultSettings = {};
|
||||
|
@ -56,11 +61,11 @@ for (const key of Object.keys(SETTINGS)) {
|
|||
}
|
||||
|
||||
const LEVEL_HANDLERS = {
|
||||
"device": new DeviceSettingsHandler(featureNames),
|
||||
"room-device": new RoomDeviceSettingsHandler(),
|
||||
"room-account": new RoomAccountSettingsHandler(),
|
||||
"account": new AccountSettingsHandler(),
|
||||
"room": new RoomSettingsHandler(),
|
||||
"device": new DeviceSettingsHandler(featureNames, defaultWatchManager),
|
||||
"room-device": new RoomDeviceSettingsHandler(defaultWatchManager),
|
||||
"room-account": new RoomAccountSettingsHandler(defaultWatchManager),
|
||||
"account": new AccountSettingsHandler(defaultWatchManager),
|
||||
"room": new RoomSettingsHandler(defaultWatchManager),
|
||||
"config": new ConfigSettingsHandler(),
|
||||
"default": new DefaultSettingsHandler(defaultSettings, invertedDefaultSettings),
|
||||
};
|
||||
|
@ -98,6 +103,109 @@ const LEVEL_ORDER = [
|
|||
* be enabled).
|
||||
*/
|
||||
export default class SettingsStore {
|
||||
// We support watching settings for changes, and do this by tracking which callbacks have
|
||||
// been given to us. We end up returning the callbackRef to the caller so they can unsubscribe
|
||||
// at a later point.
|
||||
//
|
||||
// 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 => { 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, at any level.
|
||||
* 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 the
|
||||
* returned reference to later unsubscribe from updates.
|
||||
* @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. Five arguments can be expected: the setting name, the room ID (may be null),
|
||||
* the level the change happened at, the new value at the given level, and finally the new
|
||||
* value for the setting regardless of level. The callback is responsible for determining
|
||||
* if the change in value is worthwhile enough to react upon.
|
||||
* @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}`;
|
||||
|
||||
const localizedCallback = (changedInRoomId, atLevel, newValAtLevel) => {
|
||||
const newValue = SettingsStore.getValue(originalSettingName);
|
||||
callbackFn(originalSettingName, changedInRoomId, atLevel, newValAtLevel, newValue);
|
||||
};
|
||||
|
||||
console.log(`Starting watcher for ${settingName}@${roomId || '<null room>'}`);
|
||||
SettingsStore._watchers[watcherId] = localizedCallback;
|
||||
defaultWatchManager.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;
|
||||
|
||||
defaultWatchManager.unwatchSetting(SettingsStore._watchers[watcherReference]);
|
||||
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, newValueAtLevel, 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, newValueAtLevel, newValue) => {
|
||||
dis.dispatch({
|
||||
action: 'setting_updated',
|
||||
settingName,
|
||||
roomId: inRoomId,
|
||||
level,
|
||||
newValueAtLevel,
|
||||
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.
|
||||
|
|
61
src/settings/WatchManager.js
Normal file
61
src/settings/WatchManager.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
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, atLevel, newValueAtLevel) {
|
||||
// Dev note: We could avoid raising changes for ultimately inconsequential changes, but
|
||||
// we also don't have a reliable way to get the old value of a setting. Instead, we'll just
|
||||
// let it fall through regardless and let the receiver dedupe if they want to.
|
||||
|
||||
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, atLevel, newValueAtLevel);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,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 {SettingLevel} from "../SettingsStore";
|
||||
|
||||
/**
|
||||
* 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(watchManager) {
|
||||
super();
|
||||
|
||||
this._watchers = 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, SettingLevel.ACCOUNT, 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())) {
|
||||
const val = event.getContent()[settingName];
|
||||
this._watchers.notifyUpdate(settingName, null, SettingLevel.ACCOUNT, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getValue(settingName, roomId) {
|
||||
// Special case URL previews
|
||||
if (settingName === "urlPreviewsEnabled") {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {SettingLevel} from "../SettingsStore";
|
||||
|
||||
/**
|
||||
* Gets and sets settings at the "device" level for the current device.
|
||||
|
@ -27,10 +29,12 @@ export default class DeviceSettingsHandler extends SettingsHandler {
|
|||
/**
|
||||
* Creates a new device settings handler
|
||||
* @param {string[]} featureNames The names of known features.
|
||||
* @param {WatchManager} watchManager The watch manager to notify updates to
|
||||
*/
|
||||
constructor(featureNames) {
|
||||
constructor(featureNames, watchManager) {
|
||||
super();
|
||||
this._featureNames = featureNames;
|
||||
this._watchers = watchManager;
|
||||
}
|
||||
|
||||
getValue(settingName, roomId) {
|
||||
|
@ -66,18 +70,22 @@ export default class DeviceSettingsHandler extends SettingsHandler {
|
|||
// Special case notifications
|
||||
if (settingName === "notificationsEnabled") {
|
||||
localStorage.setItem("notifications_enabled", newValue);
|
||||
this._watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue);
|
||||
return Promise.resolve();
|
||||
} else if (settingName === "notificationBodyEnabled") {
|
||||
localStorage.setItem("notifications_body_enabled", newValue);
|
||||
this._watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue);
|
||||
return Promise.resolve();
|
||||
} else if (settingName === "audioNotificationsEnabled") {
|
||||
localStorage.setItem("audio_notifications_enabled", newValue);
|
||||
this._watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const settings = this._getSettings() || {};
|
||||
settings[settingName] = newValue;
|
||||
localStorage.setItem("mx_local_settings", JSON.stringify(settings));
|
||||
this._watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@ -90,6 +98,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 +127,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.
|
||||
|
|
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,52 @@ 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 {SettingLevel} from "../SettingsStore";
|
||||
|
||||
/**
|
||||
* 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(watchManager) {
|
||||
super();
|
||||
|
||||
this._watchers = 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, SettingLevel.ROOM_ACCOUNT, val);
|
||||
} else if (event.getType() === "org.matrix.room.color_scheme") {
|
||||
this._watchers.notifyUpdate("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, 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())) {
|
||||
const val = event.getContent()[settingName];
|
||||
this._watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM_ACCOUNT, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getValue(settingName, roomId) {
|
||||
// Special case URL previews
|
||||
if (settingName === "urlPreviewsEnabled") {
|
||||
|
|
|
@ -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 {SettingLevel} from "../SettingsStore";
|
||||
|
||||
/**
|
||||
* Gets and sets settings at the "room-device" level for the current device in a particular
|
||||
* room.
|
||||
*/
|
||||
export default class RoomDeviceSettingsHandler extends SettingsHandler {
|
||||
constructor(watchManager) {
|
||||
super();
|
||||
|
||||
this._watchers = 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, SettingLevel.ROOM_DEVICE, 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, SettingLevel.ROOM_DEVICE, newValue);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {SettingLevel} from "../SettingsStore";
|
||||
|
||||
/**
|
||||
* Gets and sets settings at the "room" level.
|
||||
*/
|
||||
export default class RoomSettingsHandler extends SettingsHandler {
|
||||
export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandler {
|
||||
constructor(watchManager) {
|
||||
super();
|
||||
|
||||
this._watchers = 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, SettingLevel.ROOM, 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, SettingLevel.ROOM, event.getContent()[settingName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getValue(settingName, roomId) {
|
||||
// Special case URL previews
|
||||
if (settingName === "urlPreviewsEnabled") {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -19,20 +19,52 @@ import DMRoomMap from '../utils/DMRoomMap';
|
|||
import Unread from '../Unread';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
|
||||
/*
|
||||
Room sorting algorithm:
|
||||
* Always prefer to have red > grey > bold > idle
|
||||
* The room being viewed should be sticky (not jump down to the idle list)
|
||||
* When switching to a new room, sort the last sticky room to the top of the idle list.
|
||||
|
||||
The approach taken by the store is to generate an initial representation of all the
|
||||
tagged lists (accepting that it'll take a little bit longer to calculate) and make
|
||||
small changes to that over time. This results in quick changes to the room list while
|
||||
also having update operations feel more like popping/pushing to a stack.
|
||||
*/
|
||||
|
||||
const CATEGORY_RED = "red"; // Mentions in the room
|
||||
const CATEGORY_GREY = "grey"; // Unread notified messages (not mentions)
|
||||
const CATEGORY_BOLD = "bold"; // Unread messages (not notified, 'Mentions Only' rooms)
|
||||
const CATEGORY_IDLE = "idle"; // Nothing of interest
|
||||
|
||||
const CATEGORY_ORDER = [CATEGORY_RED, CATEGORY_GREY, CATEGORY_BOLD, CATEGORY_IDLE];
|
||||
const LIST_ORDERS = {
|
||||
"m.favourite": "manual",
|
||||
"im.vector.fake.invite": "recent",
|
||||
"im.vector.fake.recent": "recent",
|
||||
"im.vector.fake.direct": "recent",
|
||||
"m.lowpriority": "recent",
|
||||
"im.vector.fake.archived": "recent",
|
||||
};
|
||||
|
||||
/**
|
||||
* Identifier for the "breadcrumb" (or "sort by most important room first") algorithm.
|
||||
* Includes a provision for keeping the currently open room from flying down the room
|
||||
* list.
|
||||
* @type {string}
|
||||
*/
|
||||
const ALGO_IMPORTANCE = "importance";
|
||||
|
||||
/**
|
||||
* Identifier for classic sorting behaviour: sort by the most recent message first.
|
||||
* @type {string}
|
||||
*/
|
||||
const ALGO_RECENT = "recent";
|
||||
|
||||
/**
|
||||
* A class for storing application state for categorising rooms in
|
||||
* the RoomList.
|
||||
*/
|
||||
class RoomListStore extends Store {
|
||||
static _listOrders = {
|
||||
"m.favourite": "manual",
|
||||
"im.vector.fake.invite": "recent",
|
||||
"im.vector.fake.recent": "recent",
|
||||
"im.vector.fake.direct": "recent",
|
||||
"m.lowpriority": "recent",
|
||||
"im.vector.fake.archived": "recent",
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super(dis);
|
||||
|
||||
|
@ -41,78 +73,127 @@ class RoomListStore extends Store {
|
|||
this._recentsComparator = this._recentsComparator.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the sorting algorithm used by the RoomListStore.
|
||||
* @param {string} algorithm The new algorithm to use. Should be one of the ALGO_* constants.
|
||||
*/
|
||||
updateSortingAlgorithm(algorithm) {
|
||||
// Dev note: We only have two algorithms at the moment, but it isn't impossible that we want
|
||||
// multiple in the future. Also constants make things slightly clearer.
|
||||
const byImportance = algorithm === ALGO_IMPORTANCE;
|
||||
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
|
||||
this._state = {
|
||||
lists: {
|
||||
"m.server_notice": [],
|
||||
"im.vector.fake.invite": [],
|
||||
"m.favourite": [],
|
||||
"im.vector.fake.recent": [],
|
||||
"im.vector.fake.direct": [],
|
||||
"m.lowpriority": [],
|
||||
"im.vector.fake.archived": [],
|
||||
},
|
||||
ready: false,
|
||||
|
||||
// The room cache stores a mapping of roomId to cache record.
|
||||
// Each cache record is a key/value pair for various bits of
|
||||
// data used to sort the room list. Currently this stores the
|
||||
// following bits of informations:
|
||||
// "timestamp": number, The timestamp of the last relevant
|
||||
// event in the room.
|
||||
// "notifications": boolean, Whether or not the user has been
|
||||
// highlighted on any unread events.
|
||||
// "unread": boolean, Whether or not the user has any
|
||||
// unread events.
|
||||
//
|
||||
// All of the cached values are lazily loaded on read in the
|
||||
// recents comparator. When an event is received for a particular
|
||||
// room, all the cached values are invalidated - forcing the
|
||||
// next read to set new values. The entries do not expire on
|
||||
// their own.
|
||||
roomCache: {},
|
||||
const defaultLists = {
|
||||
"m.server_notice": [/* { room: js-sdk room, category: string } */],
|
||||
"im.vector.fake.invite": [],
|
||||
"m.favourite": [],
|
||||
"im.vector.fake.recent": [],
|
||||
"im.vector.fake.direct": [],
|
||||
"m.lowpriority": [],
|
||||
"im.vector.fake.archived": [],
|
||||
};
|
||||
this._state = {
|
||||
// The rooms in these arrays are ordered according to either the
|
||||
// 'recents' behaviour or 'manual' behaviour.
|
||||
lists: defaultLists,
|
||||
presentationLists: defaultLists, // like `lists`, but with arrays of rooms instead
|
||||
ready: false,
|
||||
stickyRoomId: null,
|
||||
orderRoomsByImportance: true,
|
||||
};
|
||||
|
||||
SettingsStore.monitorSetting('RoomList.orderByImportance', null);
|
||||
SettingsStore.monitorSetting('feature_custom_tags', null);
|
||||
}
|
||||
|
||||
_setState(newState) {
|
||||
// If we're changing the lists, transparently change the presentation lists (which
|
||||
// is given to requesting components). This dramatically simplifies our code elsewhere
|
||||
// while also ensuring we don't need to update all the calling components to support
|
||||
// categories.
|
||||
if (newState['lists']) {
|
||||
const presentationLists = {};
|
||||
for (const key of Object.keys(newState['lists'])) {
|
||||
presentationLists[key] = newState['lists'][key].map((e) => e.room);
|
||||
}
|
||||
newState['presentationLists'] = presentationLists;
|
||||
}
|
||||
this._state = Object.assign(this._state, newState);
|
||||
this.__emitChange();
|
||||
}
|
||||
|
||||
__onDispatch(payload) {
|
||||
const logicallyReady = this._matrixClient && this._state.ready;
|
||||
switch (payload.action) {
|
||||
case 'setting_updated': {
|
||||
if (payload.settingName === 'RoomList.orderByImportance') {
|
||||
this.updateSortingAlgorithm(payload.newValue === true ? ALGO_IMPORTANCE : ALGO_RECENT);
|
||||
} else if (payload.settingName === 'feature_custom_tags') {
|
||||
this._setState({tagsEnabled: payload.newValue});
|
||||
this._generateInitialRoomLists(); // Tags means we have to start from scratch
|
||||
}
|
||||
}
|
||||
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._generateRoomLists();
|
||||
|
||||
const algorithm = SettingsStore.getValue("RoomList.orderByImportance")
|
||||
? ALGO_IMPORTANCE : ALGO_RECENT;
|
||||
this.updateSortingAlgorithm(algorithm);
|
||||
}
|
||||
break;
|
||||
case 'MatrixActions.Room.receipt': {
|
||||
if (!logicallyReady) break;
|
||||
|
||||
// First see if the receipt event is for our own user. If it was, trigger
|
||||
// a room update (we probably read the room on a different device).
|
||||
const myUserId = this._matrixClient.getUserId();
|
||||
for (const eventId of Object.keys(payload.event.getContent())) {
|
||||
const receiptUsers = Object.keys(payload.event.getContent()[eventId]['m.read'] || {});
|
||||
if (receiptUsers.includes(myUserId)) {
|
||||
this._roomUpdateTriggered(payload.room.roomId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'MatrixActions.Room.tags': {
|
||||
if (!this._state.ready) break;
|
||||
this._generateRoomLists();
|
||||
if (!logicallyReady) break;
|
||||
// TODO: Figure out which rooms changed in the tag and only change those.
|
||||
// This is very blunt and wipes out the sticky room stuff
|
||||
this._generateInitialRoomLists();
|
||||
}
|
||||
break;
|
||||
case 'MatrixActions.Room.timeline': {
|
||||
if (!this._state.ready ||
|
||||
if (!logicallyReady ||
|
||||
!payload.isLiveEvent ||
|
||||
!payload.isLiveUnfilteredRoomTimelineEvent ||
|
||||
!this._eventTriggersRecentReorder(payload.event)
|
||||
) break;
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
this._clearCachedRoomState(payload.event.getRoomId());
|
||||
this._generateRoomLists();
|
||||
this._roomUpdateTriggered(payload.event.getRoomId());
|
||||
}
|
||||
break;
|
||||
// When an event is decrypted, it could mean we need to reorder the room
|
||||
// list because we now know the type of the event.
|
||||
case 'MatrixActions.Event.decrypted': {
|
||||
// We may not have synced or done an initial generation of the lists
|
||||
if (!this._matrixClient || !this._state.ready) break;
|
||||
if (!logicallyReady) break;
|
||||
|
||||
const roomId = payload.event.getRoomId();
|
||||
|
||||
|
@ -129,52 +210,51 @@ class RoomListStore extends Store {
|
|||
|
||||
// Either this event was not added to the live timeline (e.g. pagination)
|
||||
// or it doesn't affect the ordering of the room list.
|
||||
if (liveTimeline !== eventTimeline ||
|
||||
!this._eventTriggersRecentReorder(payload.event)
|
||||
) break;
|
||||
if (liveTimeline !== eventTimeline || !this._eventTriggersRecentReorder(payload.event)) {
|
||||
break;
|
||||
}
|
||||
|
||||
this._clearCachedRoomState(payload.event.getRoomId());
|
||||
this._generateRoomLists();
|
||||
this._roomUpdateTriggered(roomId);
|
||||
}
|
||||
break;
|
||||
case 'MatrixActions.accountData': {
|
||||
if (!logicallyReady) break;
|
||||
if (payload.event_type !== 'm.direct') break;
|
||||
this._generateRoomLists();
|
||||
}
|
||||
break;
|
||||
case 'MatrixActions.Room.accountData': {
|
||||
if (payload.event_type === 'm.fully_read') {
|
||||
this._clearCachedRoomState(payload.room.roomId);
|
||||
this._generateRoomLists();
|
||||
}
|
||||
// TODO: Figure out which rooms changed in the direct chat and only change those.
|
||||
// This is very blunt and wipes out the sticky room stuff
|
||||
this._generateInitialRoomLists();
|
||||
}
|
||||
break;
|
||||
case 'MatrixActions.Room.myMembership': {
|
||||
this._generateRoomLists();
|
||||
if (!logicallyReady) break;
|
||||
this._roomUpdateTriggered(payload.room.roomId, true);
|
||||
}
|
||||
break;
|
||||
// This could be a new room that we've been invited to, joined or created
|
||||
// we won't get a RoomMember.membership for these cases if we're not already
|
||||
// a member.
|
||||
case 'MatrixActions.Room': {
|
||||
if (!this._state.ready || !this._matrixClient.credentials.userId) break;
|
||||
this._generateRoomLists();
|
||||
}
|
||||
break;
|
||||
case 'RoomListActions.tagRoom.pending': {
|
||||
// XXX: we only show one optimistic update at any one time.
|
||||
// Ideally we should be making a list of in-flight requests
|
||||
// that are backed by transaction IDs. Until the js-sdk
|
||||
// supports this, we're stuck with only being able to use
|
||||
// the most recent optimistic update.
|
||||
this._generateRoomLists(payload.request);
|
||||
}
|
||||
break;
|
||||
case 'RoomListActions.tagRoom.failure': {
|
||||
// Reset state according to js-sdk
|
||||
this._generateRoomLists();
|
||||
if (!logicallyReady) break;
|
||||
this._roomUpdateTriggered(payload.room.roomId, true);
|
||||
}
|
||||
break;
|
||||
// TODO: Re-enable optimistic updates when we support dragging again
|
||||
// case 'RoomListActions.tagRoom.pending': {
|
||||
// if (!logicallyReady) break;
|
||||
// // XXX: we only show one optimistic update at any one time.
|
||||
// // Ideally we should be making a list of in-flight requests
|
||||
// // that are backed by transaction IDs. Until the js-sdk
|
||||
// // supports this, we're stuck with only being able to use
|
||||
// // the most recent optimistic update.
|
||||
// console.log("!! Optimistic tag: ", payload);
|
||||
// }
|
||||
// break;
|
||||
// case 'RoomListActions.tagRoom.failure': {
|
||||
// if (!logicallyReady) break;
|
||||
// // Reset state according to js-sdk
|
||||
// console.log("!! Optimistic tag failure: ", payload);
|
||||
// }
|
||||
// break;
|
||||
case 'on_logged_out': {
|
||||
// Reset state without pushing an update to the view, which generally assumes that
|
||||
// the matrix client isn't `null` and so causing a re-render will cause NPEs.
|
||||
|
@ -182,10 +262,212 @@ class RoomListStore extends Store {
|
|||
this._matrixClient = null;
|
||||
}
|
||||
break;
|
||||
case 'view_room': {
|
||||
if (!logicallyReady) break;
|
||||
|
||||
// Note: it is important that we set a new stickyRoomId before setting the old room
|
||||
// to IDLE. If we don't, the wrong room gets counted as sticky.
|
||||
const currentStickyId = this._state.stickyRoomId;
|
||||
this._setState({stickyRoomId: payload.room_id});
|
||||
if (currentStickyId) {
|
||||
this._setRoomCategory(this._matrixClient.getRoom(currentStickyId), CATEGORY_IDLE);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_generateRoomLists(optimisticRequest) {
|
||||
_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 || 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;
|
||||
|
||||
const category = this._calculateCategory(room);
|
||||
this._setRoomCategory(room, category);
|
||||
}
|
||||
}
|
||||
|
||||
_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));
|
||||
|
||||
// Order matters here: The DMRoomMap updates before invites
|
||||
// are accepted, so we check to see if the room is an invite
|
||||
// first, then if it is a direct chat, and finally default
|
||||
// to the "recents" list.
|
||||
const dmRoomMap = DMRoomMap.shared();
|
||||
if (myMembership === 'invite') {
|
||||
tags.push("im.vector.fake.invite");
|
||||
} else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
|
||||
tags.push("im.vector.fake.direct");
|
||||
} 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
|
||||
|
||||
const listsClone = {};
|
||||
const targetCategoryIndex = CATEGORY_ORDER.indexOf(category);
|
||||
|
||||
// Micro optimization: Support lazily loading the last timestamp in a room
|
||||
let _targetTimestamp = null;
|
||||
const targetTimestamp = () => {
|
||||
if (_targetTimestamp === null) {
|
||||
_targetTimestamp = this._tsOfNewestEvent(room);
|
||||
}
|
||||
return _targetTimestamp;
|
||||
};
|
||||
|
||||
const targetTags = this._getRecommendedTagsForRoom(room);
|
||||
const insertedIntoTags = [];
|
||||
|
||||
// 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.
|
||||
|
||||
for (const key of Object.keys(this._state.lists)) {
|
||||
const shouldHaveRoom = targetTags.includes(key);
|
||||
|
||||
// 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]) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
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});
|
||||
}
|
||||
|
||||
_generateInitialRoomLists() {
|
||||
// Log something to show that we're throwing away the old results. This is for the inevitable
|
||||
// question of "why is 100% of my CPU going towards Riot?" - a quick look at the logs would reveal
|
||||
// that something is wrong with the RoomListStore.
|
||||
console.log("Generating initial room lists");
|
||||
|
||||
const lists = {
|
||||
"m.server_notice": [],
|
||||
"im.vector.fake.invite": [],
|
||||
|
@ -196,74 +478,84 @@ class RoomListStore extends Store {
|
|||
"im.vector.fake.archived": [],
|
||||
};
|
||||
|
||||
|
||||
const dmRoomMap = DMRoomMap.shared();
|
||||
|
||||
// If somehow we dispatched a RoomListActions.tagRoom.failure before a MatrixActions.sync
|
||||
if (!this._matrixClient) return;
|
||||
// Speed optimization: Hitting the SettingsStore is expensive, so avoid that at all costs.
|
||||
let _isCustomTagsEnabled = null;
|
||||
const isCustomTagsEnabled = () => {
|
||||
if (_isCustomTagsEnabled === null) {
|
||||
_isCustomTagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags");
|
||||
}
|
||||
return _isCustomTagsEnabled;
|
||||
};
|
||||
|
||||
const isCustomTagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags");
|
||||
|
||||
this._matrixClient.getRooms().forEach((room, index) => {
|
||||
this._matrixClient.getRooms().forEach((room) => {
|
||||
const myUserId = this._matrixClient.getUserId();
|
||||
const membership = room.getMyMembership();
|
||||
const me = room.getMember(myUserId);
|
||||
|
||||
if (membership == "invite") {
|
||||
lists["im.vector.fake.invite"].push(room);
|
||||
} else if (membership == "join" || membership === "ban" || (me && me.isKicked())) {
|
||||
if (membership === "invite") {
|
||||
lists["im.vector.fake.invite"].push({room, category: CATEGORY_RED});
|
||||
} else if (membership === "join" || membership === "ban" || (me && me.isKicked())) {
|
||||
// Used to split rooms via tags
|
||||
let tagNames = Object.keys(room.tags);
|
||||
|
||||
if (optimisticRequest && optimisticRequest.room === room) {
|
||||
// Remove old tag
|
||||
tagNames = tagNames.filter((tagName) => tagName !== optimisticRequest.oldTag);
|
||||
// Add new tag
|
||||
if (optimisticRequest.newTag &&
|
||||
!tagNames.includes(optimisticRequest.newTag)
|
||||
) {
|
||||
tagNames.push(optimisticRequest.newTag);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore any m. tag names we don't know about
|
||||
tagNames = tagNames.filter((t) => {
|
||||
return (isCustomTagsEnabled && !t.startsWith('m.')) || lists[t] !== undefined;
|
||||
// Speed optimization: Avoid hitting the SettingsStore at all costs by making it the
|
||||
// last condition possible.
|
||||
return lists[t] !== undefined || (!t.startsWith('m.') && isCustomTagsEnabled());
|
||||
});
|
||||
|
||||
if (tagNames.length) {
|
||||
for (let i = 0; i < tagNames.length; i++) {
|
||||
const tagName = tagNames[i];
|
||||
lists[tagName] = lists[tagName] || [];
|
||||
lists[tagName].push(room);
|
||||
|
||||
// Default to an arbitrary category for tags which aren't ordered by recents
|
||||
let category = CATEGORY_IDLE;
|
||||
if (LIST_ORDERS[tagName] === 'recent') category = this._calculateCategory(room);
|
||||
lists[tagName].push({room, category: category});
|
||||
}
|
||||
} else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
|
||||
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
|
||||
lists["im.vector.fake.direct"].push(room);
|
||||
lists["im.vector.fake.direct"].push({room, category: this._calculateCategory(room)});
|
||||
} else {
|
||||
lists["im.vector.fake.recent"].push(room);
|
||||
lists["im.vector.fake.recent"].push({room, category: this._calculateCategory(room)});
|
||||
}
|
||||
} else if (membership === "leave") {
|
||||
lists["im.vector.fake.archived"].push(room);
|
||||
// The category of these rooms is not super important, so deprioritize it to the lowest
|
||||
// possible value.
|
||||
lists["im.vector.fake.archived"].push({room, category: CATEGORY_IDLE});
|
||||
}
|
||||
});
|
||||
|
||||
// Note: we check the settings up here instead of in the forEach or
|
||||
// in the _recentsComparator to avoid hitting the SettingsStore a few
|
||||
// thousand times.
|
||||
const pinUnread = SettingsStore.getValue("pinUnreadRooms");
|
||||
const pinMentioned = SettingsStore.getValue("pinMentionedRooms");
|
||||
// We use this cache in the recents comparator because _tsOfNewestEvent can take a while. This
|
||||
// cache only needs to survive the sort operation below and should not be implemented outside
|
||||
// of this function, otherwise the room lists will almost certainly be out of date and wrong.
|
||||
const latestEventTsCache = {}; // roomId => timestamp
|
||||
|
||||
Object.keys(lists).forEach((listKey) => {
|
||||
let comparator;
|
||||
switch (RoomListStore._listOrders[listKey]) {
|
||||
switch (LIST_ORDERS[listKey]) {
|
||||
case "recent":
|
||||
comparator = (roomA, roomB) => {
|
||||
return this._recentsComparator(roomA, roomB, pinUnread, pinMentioned);
|
||||
comparator = (entryA, entryB) => {
|
||||
return this._recentsComparator(entryA, entryB, (room) => {
|
||||
if (!room) return Number.MAX_SAFE_INTEGER; // Should only happen in tests
|
||||
|
||||
if (latestEventTsCache[room.roomId]) {
|
||||
return latestEventTsCache[room.roomId];
|
||||
}
|
||||
|
||||
const ts = this._tsOfNewestEvent(room);
|
||||
latestEventTsCache[room.roomId] = ts;
|
||||
return ts;
|
||||
});
|
||||
};
|
||||
break;
|
||||
case "manual":
|
||||
default:
|
||||
comparator = this._getManualComparator(listKey, optimisticRequest);
|
||||
comparator = this._getManualComparator(listKey);
|
||||
break;
|
||||
}
|
||||
lists[listKey].sort(comparator);
|
||||
|
@ -271,52 +563,10 @@ class RoomListStore extends Store {
|
|||
|
||||
this._setState({
|
||||
lists,
|
||||
ready: true, // Ready to receive updates via Room.tags events
|
||||
ready: true, // Ready to receive updates to ordering
|
||||
});
|
||||
}
|
||||
|
||||
_updateCachedRoomState(roomId, type, value) {
|
||||
const roomCache = this._state.roomCache;
|
||||
if (!roomCache[roomId]) roomCache[roomId] = {};
|
||||
|
||||
if (typeof value !== "undefined") roomCache[roomId][type] = value;
|
||||
else delete roomCache[roomId][type];
|
||||
|
||||
this._setState({roomCache});
|
||||
}
|
||||
|
||||
_clearCachedRoomState(roomId) {
|
||||
const roomCache = this._state.roomCache;
|
||||
delete roomCache[roomId];
|
||||
this._setState({roomCache});
|
||||
}
|
||||
|
||||
_getRoomState(room, type) {
|
||||
const roomId = room.roomId;
|
||||
const roomCache = this._state.roomCache;
|
||||
if (roomCache[roomId] && typeof roomCache[roomId][type] !== 'undefined') {
|
||||
return roomCache[roomId][type];
|
||||
}
|
||||
|
||||
if (type === "timestamp") {
|
||||
const ts = this._tsOfNewestEvent(room);
|
||||
this._updateCachedRoomState(roomId, "timestamp", ts);
|
||||
return ts;
|
||||
} else if (type === "unread-muted") {
|
||||
const unread = Unread.doesRoomHaveUnreadMessages(room);
|
||||
this._updateCachedRoomState(roomId, "unread-muted", unread);
|
||||
return unread;
|
||||
} else if (type === "unread") {
|
||||
const unread = room.getUnreadNotificationCount() > 0;
|
||||
this._updateCachedRoomState(roomId, "unread", unread);
|
||||
return unread;
|
||||
} else if (type === "notifications") {
|
||||
const notifs = room.getUnreadNotificationCount("highlight") > 0;
|
||||
this._updateCachedRoomState(roomId, "notifications", notifs);
|
||||
return notifs;
|
||||
} else throw new Error("Unrecognized room cache type: " + type);
|
||||
}
|
||||
|
||||
_eventTriggersRecentReorder(ev) {
|
||||
return ev.getTs() && (
|
||||
Unread.eventTriggersUnreadCount(ev) ||
|
||||
|
@ -325,6 +575,10 @@ class RoomListStore extends Store {
|
|||
}
|
||||
|
||||
_tsOfNewestEvent(room) {
|
||||
// Apparently we can have rooms without timelines, at least under testing
|
||||
// environments. Just return MAX_INT when this happens.
|
||||
if (!room || !room.timeline) return Number.MAX_SAFE_INTEGER;
|
||||
|
||||
for (let i = room.timeline.length - 1; i >= 0; --i) {
|
||||
const ev = room.timeline[i];
|
||||
if (this._eventTriggersRecentReorder(ev)) {
|
||||
|
@ -342,53 +596,44 @@ class RoomListStore extends Store {
|
|||
}
|
||||
}
|
||||
|
||||
_recentsComparator(roomA, roomB, pinUnread, pinMentioned) {
|
||||
// We try and set the ordering to be Mentioned > Unread > Recent
|
||||
// assuming the user has the right settings, of course.
|
||||
|
||||
const timestampA = this._getRoomState(roomA, "timestamp");
|
||||
const timestampB = this._getRoomState(roomB, "timestamp");
|
||||
const timestampDiff = timestampB - timestampA;
|
||||
|
||||
if (pinMentioned) {
|
||||
const mentionsA = this._getRoomState(roomA, "notifications");
|
||||
const mentionsB = this._getRoomState(roomB, "notifications");
|
||||
if (mentionsA && !mentionsB) return -1;
|
||||
if (!mentionsA && mentionsB) return 1;
|
||||
|
||||
// If they both have notifications, sort by timestamp.
|
||||
// If neither have notifications (the fourth check not shown
|
||||
// here), then try and sort by unread messages and finally by
|
||||
// timestamp.
|
||||
if (mentionsA && mentionsB) return timestampDiff;
|
||||
_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;
|
||||
}
|
||||
|
||||
if (pinUnread) {
|
||||
let unreadA = this._getRoomState(roomA, "unread");
|
||||
let unreadB = this._getRoomState(roomB, "unread");
|
||||
if (unreadA && !unreadB) return -1;
|
||||
if (!unreadA && unreadB) return 1;
|
||||
const mentions = room.getUnreadNotificationCount("highlight") > 0;
|
||||
if (mentions) return CATEGORY_RED;
|
||||
|
||||
// If they both have unread messages, sort by timestamp
|
||||
// If nether have unread message (the fourth check not shown
|
||||
// here), then just sort by timestamp anyways.
|
||||
if (unreadA && unreadB) return timestampDiff;
|
||||
let unread = room.getUnreadNotificationCount() > 0;
|
||||
if (unread) return CATEGORY_GREY;
|
||||
|
||||
// Unread can also mean "unread without badge", which is
|
||||
// different from what the above checks for. We're also
|
||||
// going to sort those here.
|
||||
unreadA = this._getRoomState(roomA, "unread-muted");
|
||||
unreadB = this._getRoomState(roomB, "unread-muted");
|
||||
if (unreadA && !unreadB) return -1;
|
||||
if (!unreadA && unreadB) return 1;
|
||||
unread = Unread.doesRoomHaveUnreadMessages(room);
|
||||
if (unread) return CATEGORY_BOLD;
|
||||
|
||||
// If they both have unread messages, sort by timestamp
|
||||
// If nether have unread message (the fourth check not shown
|
||||
// here), then just sort by timestamp anyways.
|
||||
if (unreadA && unreadB) return timestampDiff;
|
||||
return CATEGORY_IDLE;
|
||||
}
|
||||
|
||||
_recentsComparator(entryA, entryB, tsOfNewestEventFn) {
|
||||
const roomA = entryA.room;
|
||||
const roomB = entryB.room;
|
||||
const categoryA = entryA.category;
|
||||
const categoryB = entryB.category;
|
||||
|
||||
if (categoryA !== categoryB) {
|
||||
const idxA = CATEGORY_ORDER.indexOf(categoryA);
|
||||
const idxB = CATEGORY_ORDER.indexOf(categoryB);
|
||||
if (idxA > idxB) return 1;
|
||||
if (idxA < idxB) return -1;
|
||||
return 0; // Technically not possible
|
||||
}
|
||||
|
||||
return timestampDiff;
|
||||
const timestampA = tsOfNewestEventFn(roomA);
|
||||
const timestampB = tsOfNewestEventFn(roomB);
|
||||
return timestampB - timestampA;
|
||||
}
|
||||
|
||||
_lexicographicalComparator(roomA, roomB) {
|
||||
|
@ -396,7 +641,10 @@ class RoomListStore extends Store {
|
|||
}
|
||||
|
||||
_getManualComparator(tagName, optimisticRequest) {
|
||||
return (roomA, roomB) => {
|
||||
return (entryA, entryB) => {
|
||||
const roomA = entryA.room;
|
||||
const roomB = entryB.room;
|
||||
|
||||
let metaA = roomA.tags[tagName];
|
||||
let metaB = roomB.tags[tagName];
|
||||
|
||||
|
@ -404,8 +652,8 @@ class RoomListStore extends Store {
|
|||
if (optimisticRequest && roomB === optimisticRequest.room) metaB = optimisticRequest.metaData;
|
||||
|
||||
// Make sure the room tag has an order element, if not set it to be the bottom
|
||||
const a = metaA ? metaA.order : undefined;
|
||||
const b = metaB ? metaB.order : undefined;
|
||||
const a = metaA ? Number(metaA.order) : undefined;
|
||||
const b = metaB ? Number(metaB.order) : undefined;
|
||||
|
||||
// Order undefined room tag orders to the bottom
|
||||
if (a === undefined && b !== undefined) {
|
||||
|
@ -414,12 +662,12 @@ 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);
|
||||
};
|
||||
}
|
||||
|
||||
getRoomLists() {
|
||||
return this._state.lists;
|
||||
return this._state.presentationLists;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue