Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups

This commit is contained in:
David Baker 2018-10-29 16:20:45 +00:00
commit 8ac62f8c12
81 changed files with 776 additions and 276 deletions

View file

@ -7,11 +7,8 @@ src/component-index.js
src/components/structures/BottomLeftMenu.js
src/components/structures/CompatibilityPage.js
src/components/structures/CreateRoom.js
src/components/structures/HomePage.js
src/components/structures/LeftPanel.js
src/components/structures/LoggedInView.js
src/components/structures/login/ForgotPassword.js
src/components/structures/LoginBox.js
src/components/structures/MessagePanel.js
src/components/structures/NotificationPanel.js
src/components/structures/RoomDirectory.js
@ -22,22 +19,17 @@ src/components/structures/SearchBox.js
src/components/structures/TimelinePanel.js
src/components/structures/UploadBar.js
src/components/structures/UserSettings.js
src/components/structures/ViewSource.js
src/components/views/avatars/BaseAvatar.js
src/components/views/avatars/MemberAvatar.js
src/components/views/create_room/RoomAlias.js
src/components/views/dialogs/ChangelogDialog.js
src/components/views/dialogs/DeactivateAccountDialog.js
src/components/views/dialogs/SetPasswordDialog.js
src/components/views/dialogs/UnknownDeviceDialog.js
src/components/views/directory/NetworkDropdown.js
src/components/views/elements/AddressSelector.js
src/components/views/elements/DeviceVerifyButtons.js
src/components/views/elements/DirectorySearchBox.js
src/components/views/elements/ImageView.js
src/components/views/elements/InlineSpinner.js
src/components/views/elements/MemberEventListSummary.js
src/components/views/elements/Spinner.js
src/components/views/elements/TintableSvg.js
src/components/views/elements/UserSelector.js
src/components/views/globals/MatrixToolbar.js
@ -90,7 +82,6 @@ src/MatrixClientPeg.js
src/Modal.js
src/notifications/ContentRules.js
src/notifications/PushRuleVectorState.js
src/notifications/StandardActions.js
src/notifications/VectorPushRulesDefinitions.js
src/Notifier.js
src/PlatformPeg.js
@ -111,7 +102,6 @@ src/utils/MultiInviter.js
src/utils/Receipt.js
src/VectorConferenceHandler.js
src/Velociraptor.js
src/VelocityBounce.js
src/WhoIsTyping.js
src/wrappers/withMatrixClient.js
test/components/structures/login/Registration-test.js

View file

@ -27,6 +27,8 @@ npm run build
npm run test
popd
if [ "$TRAVIS_BRANCH" != "experimental" ]
then
# run end to end tests
git clone https://github.com/matrix-org/matrix-react-end-to-end-tests.git --branch master
pushd matrix-react-end-to-end-tests
@ -36,3 +38,4 @@ ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web
./install.sh
./run.sh --travis
popd
fi

View file

@ -1,3 +1,47 @@
Changes in [0.14.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.2) (2018-10-29)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.2-rc.1...v0.14.2)
* Fix autoreplacement of ascii emoji
[\#2258](https://github.com/matrix-org/matrix-react-sdk/pull/2258)
Changes in [0.14.2-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.2-rc.1) (2018-10-24)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.1...v0.14.2-rc.1)
* Update from Weblate.
[\#2244](https://github.com/matrix-org/matrix-react-sdk/pull/2244)
* Show the group member list again
[\#2223](https://github.com/matrix-org/matrix-react-sdk/pull/2223)
* lint: make colorScheme camel case
[\#2237](https://github.com/matrix-org/matrix-react-sdk/pull/2237)
* Change leave room button text, OK -> Leave
[\#2236](https://github.com/matrix-org/matrix-react-sdk/pull/2236)
* Move all dialog buttons to the right and fix their order
[\#2231](https://github.com/matrix-org/matrix-react-sdk/pull/2231)
* Add a bit of text to explain the purpose of the RoomPreviewSpinner
[\#2225](https://github.com/matrix-org/matrix-react-sdk/pull/2225)
* Move the login box from the left sidebar to where the composer is
[\#2219](https://github.com/matrix-org/matrix-react-sdk/pull/2219)
* Fix an error where React doesn't like value=null on a select
[\#2230](https://github.com/matrix-org/matrix-react-sdk/pull/2230)
* add missing sticker translation
[\#2216](https://github.com/matrix-org/matrix-react-sdk/pull/2216)
* Support m.login.terms during registration
[\#2221](https://github.com/matrix-org/matrix-react-sdk/pull/2221)
* Don't show the invite nag bar when peeking
[\#2220](https://github.com/matrix-org/matrix-react-sdk/pull/2220)
* Apply the user's tint once the MatrixClientPeg is moderately ready
[\#2214](https://github.com/matrix-org/matrix-react-sdk/pull/2214)
* Make rageshake use less memory
[\#2217](https://github.com/matrix-org/matrix-react-sdk/pull/2217)
* Fix autocomplete
[\#2212](https://github.com/matrix-org/matrix-react-sdk/pull/2212)
* Explain feature states in a lot more detail
[\#2211](https://github.com/matrix-org/matrix-react-sdk/pull/2211)
* Fix various lint errors
[\#2213](https://github.com/matrix-org/matrix-react-sdk/pull/2213)
Changes in [0.14.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.1) (2018-10-19)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.0...v0.14.1)

View file

@ -199,12 +199,25 @@ module.exports = function (config) {
'matrix-react-sdk': path.resolve('test/skinned-sdk.js'),
'sinon': 'sinon/pkg/sinon.js',
// To make webpack happy
// Related: https://github.com/request/request/issues/1529
// (there's no mock available for fs, so we fake a mock by using
// an in-memory version of fs)
"fs": "memfs",
},
modules: [
path.resolve('./test'),
"node_modules"
],
},
node: {
// Because webpack is made of fail
// https://github.com/request/request/issues/1529
// Note: 'mock' is the new 'empty'
net: 'mock',
tls: 'mock'
},
devtool: 'inline-source-map',
externals: {
// Don't try to bundle electron: leave it as a commonjs dependency

View file

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "0.14.1",
"version": "0.14.2",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -76,6 +76,7 @@
"lodash": "^4.13.1",
"lolex": "2.3.2",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
"memfs": "^2.10.1",
"optimist": "^0.6.1",
"pako": "^1.0.5",
"prop-types": "^15.5.8",
@ -100,7 +101,7 @@
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.26.3",
"babel-eslint": "^6.1.2",
"babel-eslint": "^10.0.1",
"babel-loader": "^7.1.5",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
@ -114,9 +115,9 @@
"babel-preset-react": "^6.24.1",
"chokidar": "^1.6.1",
"concurrently": "^4.0.1",
"eslint": "^3.13.1",
"eslint": "^5.8.0",
"eslint-config-google": "^0.7.1",
"eslint-plugin-babel": "^4.1.2",
"eslint-plugin-babel": "^5.2.1",
"eslint-plugin-flowtype": "^2.30.0",
"eslint-plugin-react": "^7.7.0",
"estree-walker": "^0.5.0",

View file

@ -170,8 +170,7 @@ textarea {
font-weight: 300;
font-size: 15px;
position: relative;
padding-left: 58px;
padding-bottom: 36px;
padding: 0 58px 36px;
width: 60%;
max-width: 704px;
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2);
@ -216,14 +215,13 @@ textarea {
}
.mx_Dialog_content {
margin: 24px 58px 68px 0;
margin: 24px 0 68px;
font-size: 14px;
color: $primary-fg-color;
word-wrap: break-word;
}
.mx_Dialog_buttons {
padding-right: 58px;
text-align: right;
}

View file

@ -25,6 +25,10 @@ limitations under the License.
line-height: 16px;
}
.mx_TagTileContextMenu_item object {
pointer-events: none;
}
.mx_TagTileContextMenu_item_icon {
padding-right: 8px;

View file

@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_DevTools_dialog {
padding-right: 58px;
}
.mx_DevTools_content {
margin: 10px 0;
}

View file

@ -14,11 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ShareDialog {
// this is to center the content
padding-right: 58px;
}
.mx_ShareDialog hr {
margin-top: 25px;
margin-bottom: 25px;

View file

@ -20,9 +20,6 @@ limitations under the License.
// is a pain in the ass. plus might as well make the dialog big given how
// important it is.
height: 100%;
// position the gemini scrollbar nicely
padding-right: 58px;
}
.mx_UnknownDeviceDialog {

View file

@ -1,5 +1,5 @@
/*
Copyright 2107 Vector Creations Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2107 Vector Creations Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -22,7 +22,6 @@ import _clamp from 'lodash/clamp';
type MessageFormat = 'rich' | 'markdown';
class HistoryItem {
// We store history items in their native format to ensure history is accurate
// and then convert them if our RTE has subsequently changed format.
value: Value;

View file

@ -78,7 +78,6 @@ class MemberEntity extends Entity {
}
class UserEntity extends Entity {
constructor(model, showInviteButton, inviteFn) {
super(model);
this.showInviteButton = Boolean(showInviteButton);

View file

@ -64,7 +64,7 @@ export function containsEmoji(str) {
* because we want to include emoji shortnames in title text
*/
function unicodeToImage(str) {
let replaceWith, unicode, alt, short, fname;
let replaceWith; let unicode; let alt; let short; let fname;
const mappedUnicode = emojione.mapUnicodeToShort();
str = str.replace(emojione.regUnicode, function(unicodeChar) {

View file

@ -25,7 +25,6 @@ import { _t } from './languageHandler';
* API on the homeserver in question with the new password.
*/
class PasswordReset {
/**
* Configure the endpoints for password resetting.
* @param {string} homeserverUrl The URL to the HS which has the account to reset.

View file

@ -23,7 +23,6 @@ const UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
const PRESENCE_STATES = ["online", "offline", "unavailable"];
class Presence {
/**
* Start listening the user activity to evaluate his presence state.
* Any state change will be sent to the Home Server.

View file

@ -32,7 +32,6 @@ export function getDisplayAliasForRoom(room) {
* return the other one. Otherwise, return null.
*/
export function getOnlyOtherMember(room, myUserId) {
if (room.currentState.getJoinedMemberCount() === 2) {
return room.getJoinedMembers().filter(function(m) {
return m.userId !== myUserId;
@ -103,7 +102,7 @@ export function guessAndSetDMRoom(room, isDirect) {
let newTarget;
if (isDirect) {
const guessedUserId = guessDMRoomTargetId(
room, MatrixClientPeg.get().getUserId()
room, MatrixClientPeg.get().getUserId(),
);
newTarget = guessedUserId;
} else {

View file

@ -22,7 +22,6 @@ const SdkConfig = require('./SdkConfig');
const MatrixClientPeg = require('./MatrixClientPeg');
class ScalarAuthClient {
constructor() {
this.scalarToken = null;
}

View file

@ -24,7 +24,6 @@ const DEFAULTS = {
};
class SdkConfig {
static get() {
return global.mxReactSdkConfig || {};
}

View file

@ -25,7 +25,6 @@ const CURRENTLY_ACTIVE_THRESHOLD_MS = 2000;
* with the app (but at a much lower frequency than mouse move events)
*/
class UserActivity {
/**
* Start listening to user activity
*/

View file

@ -3,8 +3,8 @@ const Velocity = require('velocity-vector');
// courtesy of https://github.com/julianshapiro/velocity/issues/283
// We only use easeOutBounce (easeInBounce is just sort of nonsensical)
function bounce( p ) {
let pow2,
bounce = 4;
let pow2;
let bounce = 4;
while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {
// just sets pow2

View file

@ -85,7 +85,7 @@ export default class Autocompleter {
provider
.getCompletions(query, selection, force)
.timeout(PROVIDER_COMPLETION_TIMEOUT)
.reflect()
.reflect(),
),
);

View file

@ -26,7 +26,6 @@ import { Block } from 'slate';
*/
class PlainWithPillsSerializer {
/*
* @param {String} options.pillFormat - either 'md', 'plain', 'id'
*/

View file

@ -145,7 +145,7 @@ module.exports = React.createClass({
// Get the label/tooltip to show
getLabel: function(label, show) {
if (show) {
var RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
return <RoomTooltip className="mx_BottomLeftMenu_tooltip" label={label} />;
}
},

View file

@ -16,18 +16,18 @@ limitations under the License.
'use strict';
var React = require('react');
const React = require('react');
import { _t } from '../../languageHandler';
module.exports = React.createClass({
displayName: 'CompatibilityPage',
propTypes: {
onAccept: React.PropTypes.func
onAccept: React.PropTypes.func,
},
getDefaultProps: function() {
return {
onAccept: function() {} // NOP
onAccept: function() {}, // NOP
};
},
@ -36,7 +36,6 @@ module.exports = React.createClass({
},
render: function() {
return (
<div className="mx_CompatibilityPage">
<div className="mx_CompatibilityPage_box">
@ -69,5 +68,5 @@ module.exports = React.createClass({
</div>
</div>
);
}
},
});

View file

@ -52,15 +52,14 @@ class HomePage extends React.Component {
if (this.props.teamToken && this.props.teamServerUrl) {
this.setState({
iframeSrc: `${this.props.teamServerUrl}/static/${this.props.teamToken}/home.html`
iframeSrc: `${this.props.teamServerUrl}/static/${this.props.teamToken}/home.html`,
});
}
else {
} else {
// we use request() to inline the homepage into the react component
// so that it can inherit CSS and theming easily rather than mess around
// with iframes and trying to synchronise document.stylesheets.
let src = this.props.homePageUrl || 'home.html';
const src = this.props.homePageUrl || 'home.html';
request(
{ method: "GET", url: src },
@ -77,7 +76,7 @@ class HomePage extends React.Component {
body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1));
this.setState({ page: body });
}
},
);
}
}
@ -93,8 +92,7 @@ class HomePage extends React.Component {
<iframe src={ this.state.iframeSrc } />
</div>
);
}
else {
} else {
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
return (
<GeminiScrollbarWrapper autoshow={true} className="mx_HomePage">

View file

@ -28,7 +28,7 @@ import VectorConferenceHandler from '../../VectorConferenceHandler';
import SettingsStore from '../../settings/SettingsStore';
var LeftPanel = React.createClass({
const LeftPanel = React.createClass({
displayName: 'LeftPanel',
// NB. If you add props, don't forget to update
@ -218,7 +218,7 @@ var LeftPanel = React.createClass({
</aside>
</div>
);
}
},
});
module.exports = LeftPanel;

View file

@ -64,6 +64,9 @@ const LoggedInView = React.createClass({
teamToken: PropTypes.string,
// Used by the RoomView to handle joining rooms
viaServers: PropTypes.arrayOf(PropTypes.string),
// and lots and lots of other stuff.
},
@ -389,6 +392,7 @@ const LoggedInView = React.createClass({
onRegistered={this.props.onRegistered}
thirdPartyInvite={this.props.thirdPartyInvite}
oobData={this.props.roomOobData}
viaServers={this.props.viaServers}
eventPixelOffset={this.props.initialEventPixelOffset}
key={this.props.currentRoomId || 'roomview'}
disabled={this.props.middleDisabled}

View file

@ -53,5 +53,5 @@ module.exports = React.createClass({
{ loginButton }
</div>
);
}
},
});

View file

@ -840,6 +840,7 @@ export default React.createClass({
page_type: PageTypes.RoomView,
thirdPartyInvite: roomInfo.third_party_invite,
roomOobData: roomInfo.oob_data,
viaServers: roomInfo.via_servers,
};
if (roomInfo.room_alias) {
@ -1490,9 +1491,21 @@ export default React.createClass({
inviterName: params.inviter_name,
};
// on our URLs there might be a ?via=matrix.org or similar to help
// joins to the room succeed. We'll pass these through as an array
// to other levels. If there's just one ?via= then params.via is a
// single string. If someone does something like ?via=one.com&via=two.com
// then params.via is an array of strings.
let via = [];
if (params.via) {
if (typeof(params.via) === 'string') via = [params.via];
else via = params.via;
}
const payload = {
action: 'view_room',
event_id: eventId,
via_servers: via,
// If an event ID is given in the URL hash, notify RoomViewStore to mark
// it as highlighted, which will propagate to RoomView and highlight the
// associated EventTile.

View file

@ -16,18 +16,18 @@ limitations under the License.
'use strict';
var React = require('react');
const React = require('react');
var MatrixClientPeg = require('../../MatrixClientPeg');
var ContentRepo = require("matrix-js-sdk").ContentRepo;
var Modal = require('../../Modal');
var sdk = require('../../index');
var dis = require('../../dispatcher');
const MatrixClientPeg = require('../../MatrixClientPeg');
const ContentRepo = require("matrix-js-sdk").ContentRepo;
const Modal = require('../../Modal');
const sdk = require('../../index');
const dis = require('../../dispatcher');
var linkify = require('linkifyjs');
var linkifyString = require('linkifyjs/string');
var linkifyMatrix = require('../../linkify-matrix');
var sanitizeHtml = require('sanitize-html');
const linkify = require('linkifyjs');
const linkifyString = require('linkifyjs/string');
const linkifyMatrix = require('../../linkify-matrix');
const sanitizeHtml = require('sanitize-html');
import Promise from 'bluebird';
import { _t } from '../../languageHandler';
@ -46,7 +46,7 @@ module.exports = React.createClass({
getDefaultProps: function() {
return {
config: {},
}
};
},
getInitialState: function() {
@ -58,7 +58,7 @@ module.exports = React.createClass({
includeAll: false,
roomServer: null,
filterString: null,
}
};
},
componentWillMount: function() {
@ -139,8 +139,7 @@ module.exports = React.createClass({
if (
my_filter_string != this.state.filterString ||
my_server != this.state.roomServer ||
my_next_batch != this.nextBatch)
{
my_next_batch != this.nextBatch) {
// if the filter or server has changed since this request was sent,
// throw away the result (don't even clear the busy flag
// since we must still have a request in flight)
@ -163,8 +162,7 @@ module.exports = React.createClass({
if (
my_filter_string != this.state.filterString ||
my_server != this.state.roomServer ||
my_next_batch != this.nextBatch)
{
my_next_batch != this.nextBatch) {
// as above: we don't care about errors for old
// requests either
return;
@ -177,10 +175,10 @@ module.exports = React.createClass({
this.setState({ loading: false });
console.error("Failed to get publicRooms: %s", JSON.stringify(err));
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to get public room list', '', ErrorDialog, {
title: _t('Failed to get public room list'),
description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded'))
description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded')),
});
});
},
@ -193,13 +191,13 @@ module.exports = React.createClass({
* this needs SPEC-417.
*/
removeFromDirectory: function(room) {
var alias = get_display_alias_for_room(room);
var name = room.name || alias || _t('Unnamed room');
const alias = get_display_alias_for_room(room);
const name = room.name || alias || _t('Unnamed room');
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var desc;
let desc;
if (alias) {
desc = _t('Delete the room alias %(alias)s and remove %(name)s from the directory?', {alias: alias, name: name});
} else {
@ -212,9 +210,9 @@ module.exports = React.createClass({
onFinished: (should_delete) => {
if (!should_delete) return;
var Loader = sdk.getComponent("elements.Spinner");
var modal = Modal.createDialog(Loader);
var step = _t('remove %(name)s from the directory.', {name: name});
const Loader = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(Loader);
let step = _t('remove %(name)s from the directory.', {name: name});
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
if (!alias) return;
@ -229,10 +227,10 @@ module.exports = React.createClass({
console.error("Failed to " + step + ": " + err);
Modal.createTrackedDialog('Remove from Directory Error', '', ErrorDialog, {
title: _t('Error'),
description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded'))
description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded')),
});
});
}
},
});
},
@ -347,7 +345,7 @@ module.exports = React.createClass({
},
showRoom: function(room, room_alias) {
var payload = {action: 'view_room'};
const payload = {action: 'view_room'};
if (room) {
// Don't let the user view a room they won't be able to either
// peek or join: fail earlier so they don't have to click back
@ -383,16 +381,16 @@ module.exports = React.createClass({
},
getRows: function() {
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
if (!this.state.publicRooms) return [];
var rooms = this.state.publicRooms;
var rows = [];
var self = this;
var guestRead, guestJoin, perms;
for (var i = 0; i < rooms.length; i++) {
var name = rooms[i].name || get_display_alias_for_room(rooms[i]) || _t('Unnamed room');
const rooms = this.state.publicRooms;
const rows = [];
const self = this;
let guestRead; let guestJoin; let perms;
for (let i = 0; i < rooms.length; i++) {
const name = rooms[i].name || get_display_alias_for_room(rooms[i]) || _t('Unnamed room');
guestRead = null;
guestJoin = null;
@ -412,7 +410,7 @@ module.exports = React.createClass({
perms = <div className="mx_RoomDirectory_perms">{guestRead}{guestJoin}</div>;
}
var topic = rooms[i].topic || '';
let topic = rooms[i].topic || '';
topic = linkifyString(sanitizeHtml(topic));
rows.push(
@ -432,14 +430,14 @@ module.exports = React.createClass({
<div className="mx_RoomDirectory_name">{ name }</div>&nbsp;
{ perms }
<div className="mx_RoomDirectory_topic"
onClick={ function(e) { e.stopPropagation() } }
onClick={ function(e) { e.stopPropagation(); } }
dangerouslySetInnerHTML={{ __html: topic }} />
<div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(rooms[i]) }</div>
</td>
<td className="mx_RoomDirectory_roomMemberCount">
{ rooms[i].num_joined_members }
</td>
</tr>
</tr>,
);
}
return rows;
@ -577,7 +575,7 @@ module.exports = React.createClass({
</div>
</div>
);
}
},
});
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom

View file

@ -37,7 +37,7 @@ function getUnsentMessages(room) {
return room.getPendingEvents().filter(function(ev) {
return ev.status === Matrix.EventStatus.NOT_SENT;
});
};
}
module.exports = React.createClass({
displayName: 'RoomStatusBar',
@ -303,7 +303,7 @@ module.exports = React.createClass({
const errorIsMauError = Boolean(
this.state.syncStateData &&
this.state.syncStateData.error &&
this.state.syncStateData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED'
this.state.syncStateData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED',
);
return this.state.syncState === "ERROR" && !errorIsMauError;
},

View file

@ -88,6 +88,9 @@ module.exports = React.createClass({
// is the RightPanel collapsed?
collapsedRhs: PropTypes.bool,
// Servers the RoomView can use to try and assist joins
viaServers: PropTypes.arrayOf(PropTypes.string),
},
getInitialState: function() {
@ -833,7 +836,7 @@ module.exports = React.createClass({
action: 'do_after_sync_prepared',
deferred_action: {
action: 'join_room',
opts: { inviteSignUrl: signUrl },
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
},
});
@ -875,7 +878,7 @@ module.exports = React.createClass({
this.props.thirdPartyInvite.inviteSignUrl : undefined;
dis.dispatch({
action: 'join_room',
opts: { inviteSignUrl: signUrl },
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
});
return Promise.resolve();
});
@ -1670,7 +1673,7 @@ module.exports = React.createClass({
</AuxPanel>
);
let messageComposer, searchInfo;
let messageComposer; let searchInfo;
const canSpeak = (
// joined and not showing search results
myMembership === 'join' && !this.state.searchResults
@ -1703,7 +1706,7 @@ module.exports = React.createClass({
}
if (inCall) {
let zoomButton, voiceMuteButton, videoMuteButton;
let zoomButton; let voiceMuteButton; let videoMuteButton;
if (call.type === "video") {
zoomButton = (

View file

@ -72,7 +72,7 @@ module.exports = React.createClass({
function() {
this.props.onSearch(this.refs.search.value);
},
100
100,
),
onToggleCollapse: function(show) {
@ -80,8 +80,7 @@ module.exports = React.createClass({
dis.dispatch({
action: 'show_left_panel',
});
}
else {
} else {
dis.dispatch({
action: 'hide_left_panel',
});
@ -103,25 +102,24 @@ module.exports = React.createClass({
},
render: function() {
var TintableSvg = sdk.getComponent('elements.TintableSvg');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
var collapseTabIndex = this.refs.search && this.refs.search.value !== "" ? "-1" : "0";
const collapseTabIndex = this.refs.search && this.refs.search.value !== "" ? "-1" : "0";
var toggleCollapse;
let toggleCollapse;
if (this.props.collapsed) {
toggleCollapse =
<AccessibleButton className="mx_SearchBox_maximise" tabIndex={collapseTabIndex} onClick={ this.onToggleCollapse.bind(this, true) }>
<TintableSvg src="img/maximise.svg" width="10" height="16" alt={ _t("Expand panel") } />
</AccessibleButton>
}
else {
</AccessibleButton>;
} else {
toggleCollapse =
<AccessibleButton className="mx_SearchBox_minimise" tabIndex={collapseTabIndex} onClick={ this.onToggleCollapse.bind(this, false) }>
<TintableSvg src="img/minimise.svg" width="10" height="16" alt={ _t("Collapse panel") } />
</AccessibleButton>
</AccessibleButton>;
}
var searchControls;
let searchControls;
if (!this.props.collapsed) {
searchControls = [
this.state.searchTerm.length > 0 ?
@ -148,16 +146,16 @@ module.exports = React.createClass({
onChange={ this.onChange }
onKeyDown={ this._onKeyDown }
placeholder={ _t('Filter room names') }
/>
/>,
];
}
var self = this;
const self = this;
return (
<div className="mx_SearchBox">
{ searchControls }
{ toggleCollapse }
</div>
);
}
},
});

View file

@ -1309,7 +1309,7 @@ module.exports = React.createClass({
// If the olmVersion is not defined then either crypto is disabled, or
// we are using a version old version of olm. We assume the former.
let olmVersionString = "<not-enabled>";
if (olmVersion !== undefined) {
if (olmVersion) {
olmVersionString = `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
}

View file

@ -53,5 +53,5 @@ module.exports = React.createClass({
</SyntaxHighlight>
</div>
);
}
},
});

View file

@ -79,7 +79,7 @@ module.exports = React.createClass({
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
let {member, fallbackUserId, onClick, viewUserOnClick, ...otherProps} = this.props;
let userId = member ? member.userId : fallbackUserId;
const userId = member ? member.userId : fallbackUserId;
if (viewUserOnClick) {
onClick = () => {

View file

@ -66,7 +66,7 @@ export default class ChangelogDialog extends React.Component {
{this.state[repo].map(this._elementsForCommit)}
</ul>
</div>
)
);
});
const content = (
@ -83,7 +83,7 @@ export default class ChangelogDialog extends React.Component {
button={_t("Update")}
onFinished={this.props.onFinished}
/>
)
);
}
}

View file

@ -26,7 +26,6 @@ import Unread from '../../../Unread';
import classNames from 'classnames';
export default class ChatCreateOrReuseDialog extends React.Component {
constructor(props) {
super(props);
this.onFinished = this.onFinished.bind(this);

View file

@ -57,7 +57,7 @@ export default React.createClass({
let error = null;
if (!this.state.groupId) {
error = _t("Community IDs cannot be empty.");
} else if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
} else if (!/^[a-z0-9=_\-./]*$/.test(this.state.groupId)) {
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
}
this.setState({

View file

@ -625,7 +625,7 @@ export default class DevtoolsDialog extends React.Component {
let body;
if (this.state.mode) {
body = <div className="mx_DevTools_dialog">
body = <div>
<div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div>
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
<div className="mx_DevTools_label_bottom" />
@ -634,7 +634,7 @@ export default class DevtoolsDialog extends React.Component {
} else {
const classes = "mx_DevTools_RoomStateExplorer_button";
body = <div>
<div className="mx_DevTools_dialog">
<div>
<div className="mx_DevTools_label_left">{ _t('Toolbox') }</div>
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
<div className="mx_DevTools_label_bottom" />

View file

@ -153,8 +153,8 @@ export default class NetworkDropdown extends React.Component {
const sortedInstances = this.props.protocols[proto].instances;
sortedInstances.sort(function(x, y) {
const a = x.desc
const b = y.desc
const a = x.desc;
const b = y.desc;
if (a < b) {
return -1;
} else if (a > b) {
@ -208,7 +208,7 @@ export default class NetworkDropdown extends React.Component {
return <div key={key} className="mx_NetworkDropdown_networkoption" onClick={click_handler}>
{icon}
<span className="mx_NetworkDropdown_menu_network">{name}</span>
</div>
</div>;
}
render() {
@ -223,11 +223,11 @@ export default class NetworkDropdown extends React.Component {
current_value = <input type="text" className="mx_NetworkDropdown_networkoption"
ref={this.collectInputTextBox} onKeyUp={this.onInputKeyUp}
placeholder="matrix.org" // 'matrix.org' as an example of an HS name
/>
/>;
} else {
const instance = instanceForInstanceId(this.props.protocols, this.state.selectedInstanceId);
current_value = this._makeMenuOption(
this.state.selectedServer, instance, this.state.includeAll, false
this.state.selectedServer, instance, this.state.includeAll, false,
);
}

View file

@ -318,6 +318,19 @@ export default class AppTile extends React.Component {
}
this.setState({deleting: true});
// HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
if (this.refs.appFrame) {
// In practice we could just do `+= ''` to trick the browser
// into thinking the URL changed, however I can foresee this
// being optimized out by a browser. Instead, we'll just point
// the iframe at a page that is reasonably safe to use in the
// event the iframe doesn't wink away.
// This is relative to where the Riot instance is located.
this.refs.appFrame.src = 'about:blank';
}
WidgetUtils.setRoomWidget(
this.props.room.roomId,
this.props.id,

View file

@ -78,7 +78,7 @@ export default React.createClass({
},
render: function() {
let blacklistButton = null, verifyButton = null;
let blacklistButton = null; let verifyButton = null;
if (this.state.device.isBlocked()) {
blacklistButton = (

View file

@ -122,7 +122,6 @@ export default class EditableTextContainer extends React.Component {
);
}
}
}
EditableTextContainer.propTypes = {

View file

@ -16,13 +16,13 @@ limitations under the License.
'use strict';
var React = require('react');
const React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg');
const MatrixClientPeg = require('../../../MatrixClientPeg');
import {formatDate} from '../../../DateUtils';
var filesize = require('filesize');
var AccessibleButton = require('../../../components/views/elements/AccessibleButton');
const filesize = require('filesize');
const AccessibleButton = require('../../../components/views/elements/AccessibleButton');
const Modal = require('../../../Modal');
const sdk = require('../../../index');
import { _t } from '../../../languageHandler';
@ -69,24 +69,24 @@ module.exports = React.createClass({
Modal.createTrackedDialog('Confirm Redact Dialog', 'Image View', ConfirmRedactDialog, {
onFinished: (proceed) => {
if (!proceed) return;
var self = this;
const self = this;
MatrixClientPeg.get().redactEvent(
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId()
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId(),
).catch(function(e) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
// display error message stating you couldn't delete this.
var code = e.errcode || e.statusCode;
const code = e.errcode || e.statusCode;
Modal.createTrackedDialog('You cannot delete this image.', '', ErrorDialog, {
title: _t('Error'),
description: _t('You cannot delete this image. (%(code)s)', {code: code})
description: _t('You cannot delete this image. (%(code)s)', {code: code}),
});
}).done();
}
},
});
},
getName: function() {
var name = this.props.name;
let name = this.props.name;
if (name && this.props.link) {
name = <a href={ this.props.link } target="_blank" rel="noopener">{ name }</a>;
}
@ -94,7 +94,6 @@ module.exports = React.createClass({
},
render: function() {
/*
// In theory max-width: 80%, max-height: 80% on the CSS should work
// but in practice, it doesn't, so do it manually:
@ -123,7 +122,7 @@ module.exports = React.createClass({
height: displayHeight
};
*/
var style, res;
let style; let res;
if (this.props.width && this.props.height) {
style = {
@ -133,22 +132,21 @@ module.exports = React.createClass({
res = style.width + "x" + style.height + "px";
}
var size;
let size;
if (this.props.fileSize) {
size = filesize(this.props.fileSize);
}
var size_res;
let size_res;
if (size && res) {
size_res = size + ", " + res;
}
else {
} else {
size_res = size || res;
}
var showEventMeta = !!this.props.mxEvent;
const showEventMeta = !!this.props.mxEvent;
var eventMeta;
let eventMeta;
if (showEventMeta) {
// Figure out the sender, defaulting to mxid
let sender = this.props.mxEvent.getSender();
@ -163,7 +161,7 @@ module.exports = React.createClass({
</div>);
}
var eventRedact;
let eventRedact;
if (showEventMeta) {
eventRedact = (<div className="mx_ImageView_button" onClick={this.onRedactClick}>
{ _t('Remove') }
@ -201,5 +199,5 @@ module.exports = React.createClass({
</div>
</div>
);
}
},
});

View file

@ -20,14 +20,14 @@ module.exports = React.createClass({
displayName: 'InlineSpinner',
render: function() {
var w = this.props.w || 16;
var h = this.props.h || 16;
var imgClass = this.props.imgClassName || "";
const w = this.props.w || 16;
const h = this.props.h || 16;
const imgClass = this.props.imgClassName || "";
return (
<div className="mx_InlineSpinner">
<img src="img/spinner.gif" width={w} height={h} className={imgClass} />
</div>
);
}
},
});

View file

@ -54,7 +54,6 @@ function getOrCreateContainer(containerId) {
* bounding rect as the parent of PE.
*/
export default class PersistedElement extends React.Component {
static propTypes = {
// Unique identifier for this PersistedElement instance
// Any PersistedElements with the same persistKey will use

View file

@ -29,7 +29,7 @@ const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
// For URLs of matrix.to links in the timeline which have been reformatted by
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room|group)\/(([#!@+])[^\/]*)$/;
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room|group)\/(([#!@+])[^/]*)$/;
const Pill = React.createClass({
statics: {

View file

@ -16,19 +16,19 @@ limitations under the License.
'use strict';
var React = require('react');
const React = require('react');
module.exports = React.createClass({
displayName: 'Spinner',
render: function() {
var w = this.props.w || 32;
var h = this.props.h || 32;
var imgClass = this.props.imgClassName || "";
const w = this.props.w || 32;
const h = this.props.h || 32;
const imgClass = this.props.imgClassName || "";
return (
<div className="mx_Spinner">
<img src="img/spinner.gif" width={w} height={h} className={imgClass} />
</div>
);
}
},
});

View file

@ -20,7 +20,6 @@ import TintableSvg from './TintableSvg';
import AccessibleButton from './AccessibleButton';
export default class TintableSvgButton extends React.Component {
constructor(props) {
super(props);
}

View file

@ -48,7 +48,7 @@ export default React.createClass({
if (update && PlatformPeg.get()) {
PlatformPeg.get().installUpdate();
}
}
},
});
},
@ -61,7 +61,7 @@ export default React.createClass({
if (update && PlatformPeg.get()) {
PlatformPeg.get().installUpdate();
}
}
},
});
},
@ -103,5 +103,5 @@ export default React.createClass({
{action_button}
</div>
);
}
},
});

View file

@ -32,7 +32,7 @@ export default React.createClass({
getDefaultProps: function() {
return {
detail: '',
}
};
},
getStatusText: function() {
@ -59,7 +59,7 @@ export default React.createClass({
const message = this.getStatusText();
const warning = _t('Warning');
if (!'getUpdateCheckStatusEnum' in PlatformPeg.get()) {
if (!('getUpdateCheckStatusEnum' in PlatformPeg.get())) {
return <div></div>;
}
@ -87,5 +87,5 @@ export default React.createClass({
</AccessibleButton>
</div>
);
}
},
});

View file

@ -296,7 +296,7 @@ export const TermsAuthEntry = React.createClass({
return <Loader />;
}
let checkboxes = [];
const checkboxes = [];
let allChecked = true;
for (const policy of this.state.policies) {
const checked = this.state.toggledPolicies[policy.id];
@ -306,7 +306,7 @@ export const TermsAuthEntry = React.createClass({
<label key={"policy_checkbox_" + policy.id}>
<input type="checkbox" onClick={() => this._trySubmit(policy.id)} checked={checked} />
<a href={policy.url} target="_blank" rel="noopener">{ policy.name }</a>
</label>
</label>,
);
}

View file

@ -103,7 +103,7 @@ module.exports = React.createClass({
oldCanonicalAlias = this.props.canonicalAliasEvent.getContent().alias;
}
let newCanonicalAlias = this.state.canonicalAlias;
const newCanonicalAlias = this.state.canonicalAlias;
if (this.props.canSetCanonicalAlias && oldCanonicalAlias !== newCanonicalAlias) {
console.log("AliasSettings: Updating canonical alias");
@ -167,7 +167,7 @@ module.exports = React.createClass({
if (!this.props.canonicalAlias) {
this.setState({
canonicalAlias: alias
canonicalAlias: alias,
});
}
},

View file

@ -22,7 +22,7 @@ import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import isEqual from 'lodash/isEqual';
const GROUP_ID_REGEX = /\+\S+\:\S+/;
const GROUP_ID_REGEX = /\+\S+:\S+/;
module.exports = React.createClass({
displayName: 'RelatedGroupSettings',

View file

@ -33,7 +33,6 @@ import Autocompleter from '../../../autocomplete/Autocompleter';
const COMPOSER_SELECTED = 0;
export default class Autocomplete extends React.Component {
constructor(props) {
super(props);

View file

@ -107,7 +107,7 @@ module.exports = React.createClass({
// FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing?
let image = p["og:image"];
let imageMaxWidth = 100, imageMaxHeight = 100;
const imageMaxWidth = 100; const imageMaxHeight = 100;
if (image && image.startsWith("mxc://")) {
image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight);
}

View file

@ -269,7 +269,7 @@ export default class MessageComposer extends React.Component {
);
}
let e2eImg, e2eTitle, e2eClass;
let e2eImg; let e2eTitle; let e2eClass;
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
if (roomIsEncrypted) {
// FIXME: show a /!\ if there are untrusted devices in the room...
@ -429,7 +429,7 @@ export default class MessageComposer extends React.Component {
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
src="img/icon-text-cancel.svg" />
</div>
</div>
</div>;
}
return (

View file

@ -67,7 +67,7 @@ const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
const EMOJI_REGEX = new RegExp(unicodeRegexp, 'g');
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
const TYPING_USER_TIMEOUT = 10000; const TYPING_SERVER_TIMEOUT = 30000;
const ENTITY_TYPES = {
AT_ROOM_PILL: 'ATROOMPILL',
@ -544,7 +544,7 @@ export default class MessageComposerInput extends React.Component {
if (editorState.startText !== null) {
const text = editorState.startText.text;
const currentStartOffset = editorState.startOffset;
const currentStartOffset = editorState.selection.start.offset;
// Automatic replacement of plaintext emoji to Unicode emoji
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
@ -558,11 +558,11 @@ export default class MessageComposerInput extends React.Component {
const range = Range.create({
anchor: {
key: editorState.selection.startKey,
key: editorState.startText.key,
offset: currentStartOffset - emojiMatch[1].length - 1,
},
focus: {
key: editorState.selection.startKey,
key: editorState.startText.key,
offset: currentStartOffset - 1,
},
});
@ -1078,7 +1078,7 @@ export default class MessageComposerInput extends React.Component {
// only look for commands if the first block contains simple unformatted text
// i.e. no pills or rich-text formatting and begins with a /.
let cmd, commandText;
let cmd; let commandText;
const firstChild = editorState.document.nodes.get(0);
const firstGrandChild = firstChild && firstChild.nodes.get(0);
if (firstChild && firstGrandChild &&

View file

@ -93,7 +93,7 @@ module.exports = React.createClass({
},
render: function() {
let joinBlock, previewBlock;
let joinBlock; let previewBlock;
if (this.props.spinner || this.state.busy) {
const Spinner = sdk.getComponent("elements.Spinner");

View file

@ -657,31 +657,31 @@ module.exports = React.createClass({
const userLevels = powerLevels.users || {};
const powerLevelDescriptors = {
users_default: {
"users_default": {
desc: _t('The default role for new room members is'),
defaultValue: 0,
},
events_default: {
"events_default": {
desc: _t('To send messages, you must be a'),
defaultValue: 0,
},
invite: {
"invite": {
desc: _t('To invite users into the room, you must be a'),
defaultValue: 50,
},
state_default: {
"state_default": {
desc: _t('To configure the room, you must be a'),
defaultValue: 50,
},
kick: {
"kick": {
desc: _t('To kick users, you must be a'),
defaultValue: 50,
},
ban: {
"ban": {
desc: _t('To ban users, you must be a'),
defaultValue: 50,
},
redact: {
"redact": {
desc: _t('To remove other users\' messages, you must be a'),
defaultValue: 50,
},

View file

@ -290,7 +290,7 @@ module.exports = React.createClass({
for (const i in this.state.vectorContentRules.rules) {
const rule = this.state.vectorContentRules.rules[i];
let enabled, actions;
let enabled; let actions;
switch (newPushRuleVectorState) {
case PushRuleVectorState.ON:
if (rule.actions.length !== 1) {

View file

@ -14,11 +14,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import MatrixClientPeg from "./MatrixClientPeg";
export const host = "matrix.to";
export const baseUrl = `https://${host}`;
// The maximum number of servers to pick when working out which servers
// to add to permalinks. The servers are appended as ?via=example.org
const MAX_SERVER_CANDIDATES = 3;
export function makeEventPermalink(roomId, eventId) {
return `${baseUrl}/#/${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;
const serverCandidates = pickServerCandidates(roomId);
return `${permalinkBase}${encodeServerCandidates(serverCandidates)}`;
}
export function makeUserPermalink(userId) {
@ -26,9 +39,98 @@ export function makeUserPermalink(userId) {
}
export function makeRoomPermalink(roomId) {
return `${baseUrl}/#/${roomId}`;
const permalinkBase = `${baseUrl}/#/${roomId}`;
// 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;
const serverCandidates = pickServerCandidates(roomId);
return `${permalinkBase}${encodeServerCandidates(serverCandidates)}`;
}
export function makeGroupPermalink(groupId) {
return `${baseUrl}/#/${groupId}`;
}
export function encodeServerCandidates(candidates) {
if (!candidates || candidates.length === 0) return '';
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.
// 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 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) {
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));
for (let i = beforePopulation; i <= MAX_SERVER_CANDIDATES; i++) {
const idx = i - beforePopulation;
if (idx >= serversByPopulation.length) break;
candidates.push(serversByPopulation[idx]);
}
return candidates;
}

View file

@ -16,9 +16,9 @@ limitations under the License.
'use strict';
var NotificationUtils = require('./NotificationUtils');
const NotificationUtils = require('./NotificationUtils');
var encodeActions = NotificationUtils.encodeActions;
const encodeActions = NotificationUtils.encodeActions;
module.exports = {
ACTION_NOTIFY: encodeActions({notify: true}),

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const PHONE_NUMBER_REGEXP = /^[0-9 -\.]+$/;
const PHONE_NUMBER_REGEXP = /^[0-9 -.]+$/;
/*
* Do basic validation to determine if the given input could be

View file

@ -23,7 +23,6 @@ limitations under the License.
* intended to handle environmental factors for specific settings.
*/
export default class SettingController {
/**
* Gets the overridden value for the setting, if any. This must return null if the
* value is not to be overridden, otherwise it must return the new value.

View file

@ -23,7 +23,6 @@ import Unread from '../Unread';
* the RoomList.
*/
class RoomListStore extends Store {
static _listOrders = {
"m.favourite": "manual",
"im.vector.fake.invite": "recent",

View file

@ -77,7 +77,7 @@ const ALLOWED_BLOB_MIMETYPES = {
'audio/x-pn-wav': true,
'audio/flac': true,
'audio/x-flac': true,
}
};
/**
* Decrypt a file attached to a matrix event.

349
test/matrix-to-test.js Normal file
View file

@ -0,0 +1,349 @@
/*
Copyright 2018 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 expect from 'expect';
import peg from '../src/MatrixClientPeg';
import {
makeEventPermalink,
makeGroupPermalink,
makeRoomPermalink,
makeUserPermalink,
pickServerCandidates,
} from "../src/matrix-to";
import * as testUtils from "./test-utils";
describe('matrix-to', function() {
let sandbox;
beforeEach(function() {
testUtils.beforeEach(this);
sandbox = testUtils.stubClient();
peg.get().credentials = { userId: "@test:example.com" };
});
afterEach(function() {
sandbox.restore();
});
it('should pick no candidate servers when the room is not found', function() {
peg.get().getRoom = () => null;
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers.length).toBe(0);
});
it('should pick no candidate servers when the room has no members', function() {
peg.get().getRoom = () => {
return {
getJoinedMembers: () => [],
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers.length).toBe(0);
});
it('should pick a candidate server for the highest power level user in the room', function() {
peg.get().getRoom = () => {
return {
getJoinedMembers: () => [
{
userId: "@alice:pl_50",
powerLevel: 50,
},
{
userId: "@alice:pl_75",
powerLevel: 75,
},
{
userId: "@alice:pl_95",
powerLevel: 95,
},
],
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers.length).toBe(3);
expect(pickedServers[0]).toBe("pl_95");
// we don't check the 2nd and 3rd servers because that is done by the next test
});
it('should pick candidate servers based on user population', function() {
peg.get().getRoom = () => {
return {
getJoinedMembers: () => [
{
userId: "@alice:first",
powerLevel: 0,
},
{
userId: "@bob:first",
powerLevel: 0,
},
{
userId: "@charlie:first",
powerLevel: 0,
},
{
userId: "@alice:second",
powerLevel: 0,
},
{
userId: "@bob:second",
powerLevel: 0,
},
{
userId: "@charlie:third",
powerLevel: 0,
},
],
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers.length).toBe(3);
expect(pickedServers[0]).toBe("first");
expect(pickedServers[1]).toBe("second");
expect(pickedServers[2]).toBe("third");
});
it('should pick prefer candidate servers with higher power levels', function() {
peg.get().getRoom = () => {
return {
getJoinedMembers: () => [
{
userId: "@alice:first",
powerLevel: 100,
},
{
userId: "@alice:second",
powerLevel: 0,
},
{
userId: "@bob:second",
powerLevel: 0,
},
{
userId: "@charlie:third",
powerLevel: 0,
},
],
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers.length).toBe(3);
expect(pickedServers[0]).toBe("first");
expect(pickedServers[1]).toBe("second");
expect(pickedServers[2]).toBe("third");
});
it('should work with IPv4 hostnames', function() {
peg.get().getRoom = () => {
return {
getJoinedMembers: () => [
{
userId: "@alice:127.0.0.1",
powerLevel: 100,
},
],
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers.length).toBe(1);
expect(pickedServers[0]).toBe("127.0.0.1");
});
it('should work with IPv6 hostnames', function() {
peg.get().getRoom = () => {
return {
getJoinedMembers: () => [
{
userId: "@alice:[::1]",
powerLevel: 100,
},
],
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers.length).toBe(1);
expect(pickedServers[0]).toBe("[::1]");
});
it('should work with IPv4 hostnames with ports', function() {
peg.get().getRoom = () => {
return {
getJoinedMembers: () => [
{
userId: "@alice:127.0.0.1:8448",
powerLevel: 100,
},
],
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers.length).toBe(1);
expect(pickedServers[0]).toBe("127.0.0.1:8448");
});
it('should work with IPv6 hostnames with ports', function() {
peg.get().getRoom = () => {
return {
getJoinedMembers: () => [
{
userId: "@alice:[::1]:8448",
powerLevel: 100,
},
],
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers.length).toBe(1);
expect(pickedServers[0]).toBe("[::1]:8448");
});
it('should work with hostnames with ports', function() {
peg.get().getRoom = () => {
return {
getJoinedMembers: () => [
{
userId: "@alice:example.org:8448",
powerLevel: 100,
},
],
};
};
const pickedServers = pickServerCandidates("!somewhere:example.org");
expect(pickedServers).toExist();
expect(pickedServers.length).toBe(1);
expect(pickedServers[0]).toBe("example.org:8448");
});
it('should generate an event permalink for room IDs with no candidate servers', function() {
peg.get().getRoom = () => null;
const result = makeEventPermalink("!somewhere:example.org", "$something:example.com");
expect(result).toBe("https://matrix.to/#/!somewhere:example.org/$something:example.com");
});
it('should generate an event permalink for room IDs with some candidate servers', function() {
peg.get().getRoom = () => {
return {
getJoinedMembers: () => [
{
userId: "@alice:first",
powerLevel: 100,
},
{
userId: "@bob:second",
powerLevel: 0,
},
],
};
};
const result = makeEventPermalink("!somewhere:example.org", "$something:example.com");
expect(result).toBe("https://matrix.to/#/!somewhere:example.org/$something:example.com?via=first&via=second");
});
it('should generate a room permalink for room IDs with no candidate servers', function() {
peg.get().getRoom = () => null;
const result = makeRoomPermalink("!somewhere:example.org");
expect(result).toBe("https://matrix.to/#/!somewhere:example.org");
});
it('should generate a room permalink for room IDs with some candidate servers', function() {
peg.get().getRoom = () => {
return {
getJoinedMembers: () => [
{
userId: "@alice:first",
powerLevel: 100,
},
{
userId: "@bob:second",
powerLevel: 0,
},
],
};
};
const result = makeRoomPermalink("!somewhere:example.org");
expect(result).toBe("https://matrix.to/#/!somewhere:example.org?via=first&via=second");
});
// Technically disallowed but we'll test it anyways
it('should generate an event permalink for room aliases with no candidate servers', function() {
peg.get().getRoom = () => null;
const result = makeEventPermalink("#somewhere:example.org", "$something:example.com");
expect(result).toBe("https://matrix.to/#/#somewhere:example.org/$something:example.com");
});
// Technically disallowed but we'll test it anyways
it('should generate an event permalink for room aliases without candidate servers', function() {
peg.get().getRoom = () => {
return {
getJoinedMembers: () => [
{
userId: "@alice:first",
powerLevel: 100,
},
{
userId: "@bob:second",
powerLevel: 0,
},
],
};
};
const result = makeEventPermalink("#somewhere:example.org", "$something:example.com");
expect(result).toBe("https://matrix.to/#/#somewhere:example.org/$something:example.com");
});
it('should generate a room permalink for room aliases with no candidate servers', function() {
peg.get().getRoom = () => null;
const result = makeRoomPermalink("#somewhere:example.org");
expect(result).toBe("https://matrix.to/#/#somewhere:example.org");
});
it('should generate a room permalink for room aliases without candidate servers', function() {
peg.get().getRoom = () => {
return {
getJoinedMembers: () => [
{
userId: "@alice:first",
powerLevel: 100,
},
{
userId: "@bob:second",
powerLevel: 0,
},
],
};
};
const result = makeRoomPermalink("#somewhere:example.org");
expect(result).toBe("https://matrix.to/#/#somewhere:example.org");
});
it('should generate a user permalink', function() {
const result = makeUserPermalink("@someone:example.org");
expect(result).toBe("https://matrix.to/#/@someone:example.org");
});
it('should generate a group permalink', function() {
const result = makeGroupPermalink("+community:example.org");
expect(result).toBe("https://matrix.to/#/+community:example.org");
});
});