Merge branch 'experimental' into bwindels/rightpanelbelowheader

This commit is contained in:
Bruno Windels 2018-10-31 14:57:16 +01:00
commit b4dd739771
79 changed files with 756 additions and 259 deletions

View file

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

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) 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) [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'), 'matrix-react-sdk': path.resolve('test/skinned-sdk.js'),
'sinon': 'sinon/pkg/sinon.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: [ modules: [
path.resolve('./test'), path.resolve('./test'),
"node_modules" "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', devtool: 'inline-source-map',
externals: { externals: {
// Don't try to bundle electron: leave it as a commonjs dependency // Don't try to bundle electron: leave it as a commonjs dependency

View file

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

View file

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

View file

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

View file

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

View file

@ -14,11 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_ShareDialog {
// this is to center the content
padding-right: 58px;
}
.mx_ShareDialog hr { .mx_ShareDialog hr {
margin-top: 25px; margin-top: 25px;
margin-bottom: 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 // is a pain in the ass. plus might as well make the dialog big given how
// important it is. // important it is.
height: 100%; height: 100%;
// position the gemini scrollbar nicely
padding-right: 58px;
} }
.mx_UnknownDeviceDialog { .mx_UnknownDeviceDialog {
@ -51,4 +48,4 @@ limitations under the License.
.mx_UnknownDeviceDialog .mx_UnknownDeviceDialog_deviceList li { .mx_UnknownDeviceDialog .mx_UnknownDeviceDialog_deviceList li {
height: 40px; height: 40px;
border-bottom: 1px solid $primary-hairline-color; border-bottom: 1px solid $primary-hairline-color;
} }

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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 2015, 2016 OpenMarket Ltd
Copyright 2107 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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'; type MessageFormat = 'rich' | 'markdown';
class HistoryItem { class HistoryItem {
// We store history items in their native format to ensure history is accurate // We store history items in their native format to ensure history is accurate
// and then convert them if our RTE has subsequently changed format. // and then convert them if our RTE has subsequently changed format.
value: Value; value: Value;

View file

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

View file

@ -64,7 +64,7 @@ export function containsEmoji(str) {
* because we want to include emoji shortnames in title text * because we want to include emoji shortnames in title text
*/ */
function unicodeToImage(str) { function unicodeToImage(str) {
let replaceWith, unicode, alt, short, fname; let replaceWith; let unicode; let alt; let short; let fname;
const mappedUnicode = emojione.mapUnicodeToShort(); const mappedUnicode = emojione.mapUnicodeToShort();
str = str.replace(emojione.regUnicode, function(unicodeChar) { 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. * API on the homeserver in question with the new password.
*/ */
class PasswordReset { class PasswordReset {
/** /**
* Configure the endpoints for password resetting. * Configure the endpoints for password resetting.
* @param {string} homeserverUrl The URL to the HS which has the account to reset. * @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"]; const PRESENCE_STATES = ["online", "offline", "unavailable"];
class Presence { class Presence {
/** /**
* Start listening the user activity to evaluate his presence state. * Start listening the user activity to evaluate his presence state.
* Any state change will be sent to the Home Server. * Any state change will be sent to the Home Server.

View file

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

View file

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

View file

@ -24,7 +24,6 @@ const DEFAULTS = {
}; };
class SdkConfig { class SdkConfig {
static get() { static get() {
return global.mxReactSdkConfig || {}; 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) * with the app (but at a much lower frequency than mouse move events)
*/ */
class UserActivity { class UserActivity {
/** /**
* Start listening to user activity * Start listening to user activity
*/ */

View file

@ -87,7 +87,7 @@ export default {
device_display_name: address, device_display_name: address,
lang: navigator.language, lang: navigator.language,
data: data, data: data,
append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address
}); });
}, },
}; };

View file

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

View file

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

View file

@ -61,7 +61,7 @@ export default class CommunityProvider extends AutocompleteProvider {
if (command) { if (command) {
const joinedGroups = cli.getGroups().filter(({myMembership}) => myMembership === 'join'); const joinedGroups = cli.getGroups().filter(({myMembership}) => myMembership === 'join');
const groups = (await Promise.all(joinedGroups.map(async ({groupId}) => { const groups = (await Promise.all(joinedGroups.map(async({groupId}) => {
try { try {
return FlairStore.getGroupProfileCached(cli, groupId); return FlairStore.getGroupProfileCached(cli, groupId);
} catch (e) { // if FlairStore failed, fall back to just groupId } catch (e) { // if FlairStore failed, fall back to just groupId

View file

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

View file

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

View file

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

View file

@ -780,7 +780,7 @@ export default React.createClass({
), ),
button: _t("Leave"), button: _t("Leave"),
danger: this.state.isUserPrivileged, danger: this.state.isUserPrivileged,
onFinished: async (confirmed) => { onFinished: async(confirmed) => {
if (!confirmed) return; if (!confirmed) return;
this.setState({membershipBusy: true}); this.setState({membershipBusy: true});

View file

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

View file

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

View file

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

View file

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

View file

@ -840,6 +840,7 @@ export default React.createClass({
page_type: PageTypes.RoomView, page_type: PageTypes.RoomView,
thirdPartyInvite: roomInfo.third_party_invite, thirdPartyInvite: roomInfo.third_party_invite,
roomOobData: roomInfo.oob_data, roomOobData: roomInfo.oob_data,
viaServers: roomInfo.via_servers,
}; };
if (roomInfo.room_alias) { if (roomInfo.room_alias) {
@ -1489,9 +1490,21 @@ export default React.createClass({
inviterName: params.inviter_name, 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 = { const payload = {
action: 'view_room', action: 'view_room',
event_id: eventId, event_id: eventId,
via_servers: via,
// If an event ID is given in the URL hash, notify RoomViewStore to mark // 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 // it as highlighted, which will propagate to RoomView and highlight the
// associated EventTile. // associated EventTile.

View file

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

View file

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

View file

@ -90,6 +90,9 @@ module.exports = React.createClass({
// is the RightPanel collapsed? // is the RightPanel collapsed?
collapsedRhs: PropTypes.bool, collapsedRhs: PropTypes.bool,
// Servers the RoomView can use to try and assist joins
viaServers: PropTypes.arrayOf(PropTypes.string),
}, },
getInitialState: function() { getInitialState: function() {
@ -835,7 +838,7 @@ module.exports = React.createClass({
action: 'do_after_sync_prepared', action: 'do_after_sync_prepared',
deferred_action: { deferred_action: {
action: 'join_room', action: 'join_room',
opts: { inviteSignUrl: signUrl }, opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
}, },
}); });
@ -877,7 +880,7 @@ module.exports = React.createClass({
this.props.thirdPartyInvite.inviteSignUrl : undefined; this.props.thirdPartyInvite.inviteSignUrl : undefined;
dis.dispatch({ dis.dispatch({
action: 'join_room', action: 'join_room',
opts: { inviteSignUrl: signUrl }, opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
}); });
return Promise.resolve(); return Promise.resolve();
}); });
@ -1676,7 +1679,7 @@ module.exports = React.createClass({
</AuxPanel> </AuxPanel>
); );
let messageComposer, searchInfo; let messageComposer; let searchInfo;
const canSpeak = ( const canSpeak = (
// joined and not showing search results // joined and not showing search results
myMembership === 'join' && !this.state.searchResults myMembership === 'join' && !this.state.searchResults
@ -1695,7 +1698,7 @@ module.exports = React.createClass({
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
const LoginBox = sdk.getComponent('structures.LoginBox'); const LoginBox = sdk.getComponent('structures.LoginBox');
messageComposer = <LoginBox/>; messageComposer = <LoginBox />;
} }
// TODO: Why aren't we storing the term/scope/count in this format // TODO: Why aren't we storing the term/scope/count in this format
@ -1709,7 +1712,7 @@ module.exports = React.createClass({
} }
if (inCall) { if (inCall) {
let zoomButton, voiceMuteButton, videoMuteButton; let zoomButton; let voiceMuteButton; let videoMuteButton;
if (call.type === "video") { if (call.type === "video") {
zoomButton = ( zoomButton = (

View file

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

View file

@ -829,7 +829,7 @@ var TimelinePanel = React.createClass({
// 4. Also, if pos === null, the event might not be paginated - show the unread bar // 4. Also, if pos === null, the event might not be paginated - show the unread bar
const pos = this.getReadMarkerPosition(); const pos = this.getReadMarkerPosition();
return this.state.readMarkerEventId !== null && // 1. return this.state.readMarkerEventId !== null && // 1.
this.state.readMarkerEventId !== this._getCurrentReadReceipt() && // 2. this.state.readMarkerEventId !== this._getCurrentReadReceipt() && // 2.
(pos < 0 || pos === null); // 3., 4. (pos < 0 || pos === null); // 3., 4.
}, },

View file

@ -846,7 +846,7 @@ module.exports = React.createClass({
SettingsStore.getLabsFeatures().forEach((featureId) => { SettingsStore.getLabsFeatures().forEach((featureId) => {
// TODO: this ought to be a separate component so that we don't need // TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render // to rebind the onChange each time we render
const onChange = async (e) => { const onChange = async(e) => {
const checked = e.target.checked; const checked = e.target.checked;
if (featureId === "feature_lazyloading") { if (featureId === "feature_lazyloading") {
const confirmed = await this._onLazyLoadChanging(checked); const confirmed = await this._onLazyLoadChanging(checked);
@ -1299,7 +1299,7 @@ module.exports = React.createClass({
// If the olmVersion is not defined then either crypto is disabled, or // 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. // we are using a version old version of olm. We assume the former.
let olmVersionString = "<not-enabled>"; let olmVersionString = "<not-enabled>";
if (olmVersion !== undefined) { if (olmVersion) {
olmVersionString = `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`; olmVersionString = `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
} }

View file

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

View file

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

View file

@ -48,7 +48,7 @@ export default class GroupInviteTileContextMenu extends React.Component {
Modal.createTrackedDialog('Reject community invite', '', QuestionDialog, { Modal.createTrackedDialog('Reject community invite', '', QuestionDialog, {
title: _t('Reject invitation'), title: _t('Reject invitation'),
description: _t('Are you sure you want to reject the invitation?'), description: _t('Are you sure you want to reject the invitation?'),
onFinished: async (shouldLeave) => { onFinished: async(shouldLeave) => {
if (!shouldLeave) return; if (!shouldLeave) return;
// FIXME: controller shouldn't be loading a view :( // FIXME: controller shouldn't be loading a view :(

View file

@ -31,13 +31,13 @@ export default class ChangelogDialog extends React.Component {
componentDidMount() { componentDidMount() {
const version = this.props.newVersion.split('-'); const version = this.props.newVersion.split('-');
const version2 = this.props.version.split('-'); const version2 = this.props.version.split('-');
if(version == null || version2 == null) return; if (version == null || version2 == null) return;
// parse versions of form: [vectorversion]-react-[react-sdk-version]-js-[js-sdk-version] // parse versions of form: [vectorversion]-react-[react-sdk-version]-js-[js-sdk-version]
for(let i=0; i<REPOS.length; i++) { for (let i=0; i<REPOS.length; i++) {
const oldVersion = version2[2*i]; const oldVersion = version2[2*i];
const newVersion = version[2*i]; const newVersion = version[2*i];
request(`https://api.github.com/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`, (a, b, body) => { request(`https://api.github.com/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`, (a, b, body) => {
if(body == null) return; if (body == null) return;
this.setState({[REPOS[i]]: JSON.parse(body).commits}); this.setState({[REPOS[i]]: JSON.parse(body).commits});
}); });
} }
@ -66,7 +66,7 @@ export default class ChangelogDialog extends React.Component {
{this.state[repo].map(this._elementsForCommit)} {this.state[repo].map(this._elementsForCommit)}
</ul> </ul>
</div> </div>
) );
}); });
const content = ( const content = (
@ -83,7 +83,7 @@ export default class ChangelogDialog extends React.Component {
button={_t("Update")} button={_t("Update")}
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
/> />
) );
} }
} }

View file

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

View file

@ -57,7 +57,7 @@ export default React.createClass({
let error = null; let error = null;
if (!this.state.groupId) { if (!this.state.groupId) {
error = _t("Community IDs cannot be empty."); 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 '=_-./'"); error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
} }
this.setState({ this.setState({

View file

@ -625,7 +625,7 @@ export default class DevtoolsDialog extends React.Component {
let body; let body;
if (this.state.mode) { 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_left">{ this.state.mode.getLabel() }</div>
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div> <div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
<div className="mx_DevTools_label_bottom" /> <div className="mx_DevTools_label_bottom" />
@ -634,7 +634,7 @@ export default class DevtoolsDialog extends React.Component {
} else { } else {
const classes = "mx_DevTools_RoomStateExplorer_button"; const classes = "mx_DevTools_RoomStateExplorer_button";
body = <div> body = <div>
<div className="mx_DevTools_dialog"> <div>
<div className="mx_DevTools_label_left">{ _t('Toolbox') }</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_right">Room ID: { this.props.roomId }</div>
<div className="mx_DevTools_label_bottom" /> <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; const sortedInstances = this.props.protocols[proto].instances;
sortedInstances.sort(function(x, y) { sortedInstances.sort(function(x, y) {
const a = x.desc const a = x.desc;
const b = y.desc const b = y.desc;
if (a < b) { if (a < b) {
return -1; return -1;
} else if (a > b) { } 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}> return <div key={key} className="mx_NetworkDropdown_networkoption" onClick={click_handler}>
{icon} {icon}
<span className="mx_NetworkDropdown_menu_network">{name}</span> <span className="mx_NetworkDropdown_menu_network">{name}</span>
</div> </div>;
} }
render() { render() {
@ -223,11 +223,11 @@ export default class NetworkDropdown extends React.Component {
current_value = <input type="text" className="mx_NetworkDropdown_networkoption" current_value = <input type="text" className="mx_NetworkDropdown_networkoption"
ref={this.collectInputTextBox} onKeyUp={this.onInputKeyUp} ref={this.collectInputTextBox} onKeyUp={this.onInputKeyUp}
placeholder="matrix.org" // 'matrix.org' as an example of an HS name placeholder="matrix.org" // 'matrix.org' as an example of an HS name
/> />;
} else { } else {
const instance = instanceForInstanceId(this.props.protocols, this.state.selectedInstanceId); const instance = instanceForInstanceId(this.props.protocols, this.state.selectedInstanceId);
current_value = this._makeMenuOption( 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}); 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( WidgetUtils.setRoomWidget(
this.props.room.roomId, this.props.room.roomId,
this.props.id, this.props.id,

View file

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

View file

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

View file

@ -16,13 +16,13 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); const React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg'); const MatrixClientPeg = require('../../../MatrixClientPeg');
import {formatDate} from '../../../DateUtils'; import {formatDate} from '../../../DateUtils';
var filesize = require('filesize'); const filesize = require('filesize');
var AccessibleButton = require('../../../components/views/elements/AccessibleButton'); const AccessibleButton = require('../../../components/views/elements/AccessibleButton');
const Modal = require('../../../Modal'); const Modal = require('../../../Modal');
const sdk = require('../../../index'); const sdk = require('../../../index');
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -69,24 +69,24 @@ module.exports = React.createClass({
Modal.createTrackedDialog('Confirm Redact Dialog', 'Image View', ConfirmRedactDialog, { Modal.createTrackedDialog('Confirm Redact Dialog', 'Image View', ConfirmRedactDialog, {
onFinished: (proceed) => { onFinished: (proceed) => {
if (!proceed) return; if (!proceed) return;
var self = this; const self = this;
MatrixClientPeg.get().redactEvent( MatrixClientPeg.get().redactEvent(
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId() this.props.mxEvent.getRoomId(), this.props.mxEvent.getId(),
).catch(function(e) { ).catch(function(e) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
// display error message stating you couldn't delete this. // 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, { Modal.createTrackedDialog('You cannot delete this image.', '', ErrorDialog, {
title: _t('Error'), 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(); }).done();
} },
}); });
}, },
getName: function () { getName: function() {
var name = this.props.name; let name = this.props.name;
if (name && this.props.link) { if (name && this.props.link) {
name = <a href={ this.props.link } target="_blank" rel="noopener">{ name }</a>; name = <a href={ this.props.link } target="_blank" rel="noopener">{ name }</a>;
} }
@ -94,7 +94,6 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
/* /*
// In theory max-width: 80%, max-height: 80% on the CSS should work // In theory max-width: 80%, max-height: 80% on the CSS should work
// but in practice, it doesn't, so do it manually: // but in practice, it doesn't, so do it manually:
@ -123,7 +122,7 @@ module.exports = React.createClass({
height: displayHeight height: displayHeight
}; };
*/ */
var style, res; let style; let res;
if (this.props.width && this.props.height) { if (this.props.width && this.props.height) {
style = { style = {
@ -133,23 +132,22 @@ module.exports = React.createClass({
res = style.width + "x" + style.height + "px"; res = style.width + "x" + style.height + "px";
} }
var size; let size;
if (this.props.fileSize) { if (this.props.fileSize) {
size = filesize(this.props.fileSize); size = filesize(this.props.fileSize);
} }
var size_res; let size_res;
if (size && res) { if (size && res) {
size_res = size + ", " + res; size_res = size + ", " + res;
} } else {
else {
size_res = size || res; size_res = size || res;
} }
var showEventMeta = !!this.props.mxEvent; const showEventMeta = !!this.props.mxEvent;
var eventMeta; let eventMeta;
if(showEventMeta) { if (showEventMeta) {
// Figure out the sender, defaulting to mxid // Figure out the sender, defaulting to mxid
let sender = this.props.mxEvent.getSender(); let sender = this.props.mxEvent.getSender();
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
@ -163,8 +161,8 @@ module.exports = React.createClass({
</div>); </div>);
} }
var eventRedact; let eventRedact;
if(showEventMeta) { if (showEventMeta) {
eventRedact = (<div className="mx_ImageView_button" onClick={this.onRedactClick}> eventRedact = (<div className="mx_ImageView_button" onClick={this.onRedactClick}>
{ _t('Remove') } { _t('Remove') }
</div>); </div>);
@ -175,10 +173,10 @@ module.exports = React.createClass({
<div className="mx_ImageView_lhs"> <div className="mx_ImageView_lhs">
</div> </div>
<div className="mx_ImageView_content"> <div className="mx_ImageView_content">
<img src={this.props.src} style={style}/> <img src={this.props.src} style={style} />
<div className="mx_ImageView_labelWrapper"> <div className="mx_ImageView_labelWrapper">
<div className="mx_ImageView_label"> <div className="mx_ImageView_label">
<AccessibleButton className="mx_ImageView_cancel" onClick={ this.props.onFinished }><img src="img/cancel-white.svg" width="18" height="18" alt={ _t('Close') }/></AccessibleButton> <AccessibleButton className="mx_ImageView_cancel" onClick={ this.props.onFinished }><img src="img/cancel-white.svg" width="18" height="18" alt={ _t('Close') } /></AccessibleButton>
<div className="mx_ImageView_shim"> <div className="mx_ImageView_shim">
</div> </div>
<div className="mx_ImageView_name"> <div className="mx_ImageView_name">
@ -187,7 +185,7 @@ module.exports = React.createClass({
{ eventMeta } { eventMeta }
<a className="mx_ImageView_link" href={ this.props.src } download={ this.props.name } target="_blank" rel="noopener"> <a className="mx_ImageView_link" href={ this.props.src } download={ this.props.name } target="_blank" rel="noopener">
<div className="mx_ImageView_download"> <div className="mx_ImageView_download">
{ _t('Download this file') }<br/> { _t('Download this file') }<br />
<span className="mx_ImageView_size">{ size_res }</span> <span className="mx_ImageView_size">{ size_res }</span>
</div> </div>
</a> </a>
@ -201,5 +199,5 @@ module.exports = React.createClass({
</div> </div>
</div> </div>
); );
} },
}); });

View file

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

View file

@ -54,7 +54,6 @@ function getOrCreateContainer(containerId) {
* bounding rect as the parent of PE. * bounding rect as the parent of PE.
*/ */
export default class PersistedElement extends React.Component { export default class PersistedElement extends React.Component {
static propTypes = { static propTypes = {
// Unique identifier for this PersistedElement instance // Unique identifier for this PersistedElement instance
// Any PersistedElements with the same persistKey will use // 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 // For URLs of matrix.to links in the timeline which have been reformatted by
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`) // 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({ const Pill = React.createClass({
statics: { statics: {

View file

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

View file

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

View file

@ -39,7 +39,7 @@ module.exports = React.createClass({
<div className="mx_MatrixToolbar_content"> <div className="mx_MatrixToolbar_content">
{ _t('You are not receiving desktop notifications') } <a className="mx_MatrixToolbar_link" onClick={ this.onClick }> { _t('Enable them now') }</a> { _t('You are not receiving desktop notifications') } <a className="mx_MatrixToolbar_link" onClick={ this.onClick }> { _t('Enable them now') }</a>
</div> </div>
<AccessibleButton className="mx_MatrixToolbar_close" onClick={ this.hideToolbar } ><img src="img/cancel.svg" width="18" height="18" alt={_t('Close')}/></AccessibleButton> <AccessibleButton className="mx_MatrixToolbar_close" onClick={ this.hideToolbar } ><img src="img/cancel.svg" width="18" height="18" alt={_t('Close')} /></AccessibleButton>
</div> </div>
); );
}, },

View file

@ -45,10 +45,10 @@ export default React.createClass({
description: <div className="mx_MatrixToolbar_changelog">{releaseNotes}</div>, description: <div className="mx_MatrixToolbar_changelog">{releaseNotes}</div>,
button: _t("Update"), button: _t("Update"),
onFinished: (update) => { onFinished: (update) => {
if(update && PlatformPeg.get()) { if (update && PlatformPeg.get()) {
PlatformPeg.get().installUpdate(); PlatformPeg.get().installUpdate();
} }
} },
}); });
}, },
@ -58,10 +58,10 @@ export default React.createClass({
version: this.props.version, version: this.props.version,
newVersion: this.props.newVersion, newVersion: this.props.newVersion,
onFinished: (update) => { onFinished: (update) => {
if(update && PlatformPeg.get()) { if (update && PlatformPeg.get()) {
PlatformPeg.get().installUpdate(); PlatformPeg.get().installUpdate();
} }
} },
}); });
}, },
@ -103,5 +103,5 @@ export default React.createClass({
{action_button} {action_button}
</div> </div>
); );
} },
}); });

View file

@ -32,14 +32,14 @@ export default React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
detail: '', detail: '',
} };
}, },
getStatusText: function() { getStatusText: function() {
// we can't import the enum from riot-web as we don't want matrix-react-sdk // we can't import the enum from riot-web as we don't want matrix-react-sdk
// to depend on riot-web. so we grab it as a normal object via API instead. // to depend on riot-web. so we grab it as a normal object via API instead.
const updateCheckStatusEnum = PlatformPeg.get().getUpdateCheckStatusEnum(); const updateCheckStatusEnum = PlatformPeg.get().getUpdateCheckStatusEnum();
switch(this.props.status) { switch (this.props.status) {
case updateCheckStatusEnum.ERROR: case updateCheckStatusEnum.ERROR:
return _t('Error encountered (%(errorDetail)s).', { errorDetail: this.props.detail }); return _t('Error encountered (%(errorDetail)s).', { errorDetail: this.props.detail });
case updateCheckStatusEnum.CHECKING: case updateCheckStatusEnum.CHECKING:
@ -59,7 +59,7 @@ export default React.createClass({
const message = this.getStatusText(); const message = this.getStatusText();
const warning = _t('Warning'); const warning = _t('Warning');
if (!'getUpdateCheckStatusEnum' in PlatformPeg.get()) { if (!('getUpdateCheckStatusEnum' in PlatformPeg.get())) {
return <div></div>; return <div></div>;
} }
@ -83,9 +83,9 @@ export default React.createClass({
{message} {message}
</div> </div>
<AccessibleButton className="mx_MatrixToolbar_close" onClick={this.hideToolbar}> <AccessibleButton className="mx_MatrixToolbar_close" onClick={this.hideToolbar}>
<img src="img/cancel.svg" width="18" height="18" alt={_t('Close')}/> <img src="img/cancel.svg" width="18" height="18" alt={_t('Close')} />
</AccessibleButton> </AccessibleButton>
</div> </div>
); );
} },
}); });

View file

@ -296,7 +296,7 @@ export const TermsAuthEntry = React.createClass({
return <Loader />; return <Loader />;
} }
let checkboxes = []; const checkboxes = [];
let allChecked = true; let allChecked = true;
for (const policy of this.state.policies) { for (const policy of this.state.policies) {
const checked = this.state.toggledPolicies[policy.id]; const checked = this.state.toggledPolicies[policy.id];
@ -306,7 +306,7 @@ export const TermsAuthEntry = React.createClass({
<label key={"policy_checkbox_" + policy.id}> <label key={"policy_checkbox_" + policy.id}>
<input type="checkbox" onClick={() => this._trySubmit(policy.id)} checked={checked} /> <input type="checkbox" onClick={() => this._trySubmit(policy.id)} checked={checked} />
<a href={policy.url} target="_blank" rel="noopener">{ policy.name }</a> <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; oldCanonicalAlias = this.props.canonicalAliasEvent.getContent().alias;
} }
let newCanonicalAlias = this.state.canonicalAlias; const newCanonicalAlias = this.state.canonicalAlias;
if (this.props.canSetCanonicalAlias && oldCanonicalAlias !== newCanonicalAlias) { if (this.props.canSetCanonicalAlias && oldCanonicalAlias !== newCanonicalAlias) {
console.log("AliasSettings: Updating canonical alias"); console.log("AliasSettings: Updating canonical alias");
@ -167,7 +167,7 @@ module.exports = React.createClass({
if (!this.props.canonicalAlias) { if (!this.props.canonicalAlias) {
this.setState({ this.setState({
canonicalAlias: alias canonicalAlias: alias,
}); });
} }
}, },

View file

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

View file

@ -33,7 +33,6 @@ import Autocompleter from '../../../autocomplete/Autocompleter';
const COMPOSER_SELECTED = 0; const COMPOSER_SELECTED = 0;
export default class Autocomplete extends React.Component { export default class Autocomplete extends React.Component {
constructor(props) { constructor(props) {
super(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? // FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing?
let image = p["og:image"]; let image = p["og:image"];
let imageMaxWidth = 100, imageMaxHeight = 100; const imageMaxWidth = 100; const imageMaxHeight = 100;
if (image && image.startsWith("mxc://")) { if (image && image.startsWith("mxc://")) {
image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight); image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight);
} }

View file

@ -712,7 +712,7 @@ module.exports = withMatrixClient(React.createClass({
if (!member || !member.membership || member.membership === 'leave') { if (!member || !member.membership || member.membership === 'leave') {
const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId(); const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId();
const onInviteUserButton = async () => { const onInviteUserButton = async() => {
try { try {
await cli.invite(roomId, member.userId); await cli.invite(roomId, member.userId);
} catch (err) { } catch (err) {

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); const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
if (roomIsEncrypted) { if (roomIsEncrypted) {
// FIXME: show a /!\ if there are untrusted devices in the room... // 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" className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
src="img/icon-text-cancel.svg" /> src="img/icon-text-cancel.svg" />
</div> </div>
</div> </div>;
} }
return ( return (

View file

@ -67,7 +67,7 @@ const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$'); const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
const EMOJI_REGEX = new RegExp(unicodeRegexp, 'g'); 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 = { const ENTITY_TYPES = {
AT_ROOM_PILL: 'ATROOMPILL', AT_ROOM_PILL: 'ATROOMPILL',
@ -175,8 +175,8 @@ export default class MessageComposerInput extends React.Component {
// see https://github.com/ianstormtaylor/slate/issues/762#issuecomment-304855095 // see https://github.com/ianstormtaylor/slate/issues/762#issuecomment-304855095
this.direction = ''; this.direction = '';
this.plainWithMdPills = new PlainWithPillsSerializer({ pillFormat: 'md' }); this.plainWithMdPills = new PlainWithPillsSerializer({ pillFormat: 'md' });
this.plainWithIdPills = new PlainWithPillsSerializer({ pillFormat: 'id' }); this.plainWithIdPills = new PlainWithPillsSerializer({ pillFormat: 'id' });
this.plainWithPlainPills = new PlainWithPillsSerializer({ pillFormat: 'plain' }); this.plainWithPlainPills = new PlainWithPillsSerializer({ pillFormat: 'plain' });
this.md = new Md({ this.md = new Md({
@ -544,7 +544,7 @@ export default class MessageComposerInput extends React.Component {
if (editorState.startText !== null) { if (editorState.startText !== null) {
const text = editorState.startText.text; const text = editorState.startText.text;
const currentStartOffset = editorState.startOffset; const currentStartOffset = editorState.selection.start.offset;
// Automatic replacement of plaintext emoji to Unicode emoji // Automatic replacement of plaintext emoji to Unicode emoji
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) { if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
@ -558,11 +558,11 @@ export default class MessageComposerInput extends React.Component {
const range = Range.create({ const range = Range.create({
anchor: { anchor: {
key: editorState.selection.startKey, key: editorState.startText.key,
offset: currentStartOffset - emojiMatch[1].length - 1, offset: currentStartOffset - emojiMatch[1].length - 1,
}, },
focus: { focus: {
key: editorState.selection.startKey, key: editorState.startText.key,
offset: currentStartOffset - 1, 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 // only look for commands if the first block contains simple unformatted text
// i.e. no pills or rich-text formatting and begins with a /. // 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 firstChild = editorState.document.nodes.get(0);
const firstGrandChild = firstChild && firstChild.nodes.get(0); const firstGrandChild = firstChild && firstChild.nodes.get(0);
if (firstChild && firstGrandChild && if (firstChild && firstGrandChild &&
@ -1260,7 +1260,7 @@ export default class MessageComposerInput extends React.Component {
} }
}; };
selectHistory = async (up) => { selectHistory = async(up) => {
const delta = up ? -1 : 1; const delta = up ? -1 : 1;
// True if we are not currently selecting history, but composing a message // True if we are not currently selecting history, but composing a message
@ -1308,7 +1308,7 @@ export default class MessageComposerInput extends React.Component {
return true; return true;
}; };
onTab = async (e) => { onTab = async(e) => {
this.setState({ this.setState({
someCompletions: null, someCompletions: null,
}); });
@ -1330,7 +1330,7 @@ export default class MessageComposerInput extends React.Component {
up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow(); up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow();
}; };
onEscape = async (e) => { onEscape = async(e) => {
e.preventDefault(); e.preventDefault();
if (this.autocomplete) { if (this.autocomplete) {
this.autocomplete.onEscape(e); this.autocomplete.onEscape(e);
@ -1349,7 +1349,7 @@ export default class MessageComposerInput extends React.Component {
/* If passed null, restores the original editor content from state.originalEditorState. /* If passed null, restores the original editor content from state.originalEditorState.
* If passed a non-null displayedCompletion, modifies state.originalEditorState to compute new state.editorState. * If passed a non-null displayedCompletion, modifies state.originalEditorState to compute new state.editorState.
*/ */
setDisplayedCompletion = async (displayedCompletion: ?Completion): boolean => { setDisplayedCompletion = async(displayedCompletion: ?Completion): boolean => {
const activeEditorState = this.state.originalEditorState || this.state.editorState; const activeEditorState = this.state.originalEditorState || this.state.editorState;
if (displayedCompletion == null) { if (displayedCompletion == null) {

View file

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

View file

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

View file

@ -67,7 +67,7 @@ module.exports = React.createClass({
phases: { phases: {
LOADING: "LOADING", // The component is loading or sending data to the hs LOADING: "LOADING", // The component is loading or sending data to the hs
DISPLAY: "DISPLAY", // The component is ready and display data DISPLAY: "DISPLAY", // The component is ready and display data
ERROR: "ERROR", // There was an error ERROR: "ERROR", // There was an error
}, },
propTypes: { propTypes: {
@ -86,14 +86,14 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
phase: this.phases.LOADING, phase: this.phases.LOADING,
masterPushRule: undefined, // The master rule ('.m.rule.master') masterPushRule: undefined, // The master rule ('.m.rule.master')
vectorPushRules: [], // HS default push rules displayed in Vector UI vectorPushRules: [], // HS default push rules displayed in Vector UI
vectorContentRules: { // Keyword push rules displayed in Vector UI vectorContentRules: { // Keyword push rules displayed in Vector UI
vectorState: PushRuleVectorState.ON, vectorState: PushRuleVectorState.ON,
rules: [], rules: [],
}, },
externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI
externalContentRules: [], // Keyword push rules that have been defined outside Vector UI externalContentRules: [], // Keyword push rules that have been defined outside Vector UI
}; };
}, },
@ -290,7 +290,7 @@ module.exports = React.createClass({
for (const i in this.state.vectorContentRules.rules) { for (const i in this.state.vectorContentRules.rules) {
const rule = this.state.vectorContentRules.rules[i]; const rule = this.state.vectorContentRules.rules[i];
let enabled, actions; let enabled; let actions;
switch (newPushRuleVectorState) { switch (newPushRuleVectorState) {
case PushRuleVectorState.ON: case PushRuleVectorState.ON:
if (rule.actions.length !== 1) { 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. limitations under the License.
*/ */
import MatrixClientPeg from "./MatrixClientPeg";
export const host = "matrix.to"; export const host = "matrix.to";
export const baseUrl = `https://${host}`; 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) { 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) { export function makeUserPermalink(userId) {
@ -26,9 +39,98 @@ export function makeUserPermalink(userId) {
} }
export function makeRoomPermalink(roomId) { 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) { export function makeGroupPermalink(groupId) {
return `${baseUrl}/#/${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'; 'use strict';
var NotificationUtils = require('./NotificationUtils'); const NotificationUtils = require('./NotificationUtils');
var encodeActions = NotificationUtils.encodeActions; const encodeActions = NotificationUtils.encodeActions;
module.exports = { module.exports = {
ACTION_NOTIFY: encodeActions({notify: true}), 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. 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 * Do basic validation to determine if the given input could be

View file

@ -248,7 +248,7 @@ export default class SettingsStore {
if (actualValue !== undefined && actualValue !== null) return actualValue; if (actualValue !== undefined && actualValue !== null) return actualValue;
return calculatedValue; return calculatedValue;
} }
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307 /* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
/** /**
* Sets the value for a setting. The room ID is optional if the setting is not being * Sets the value for a setting. The room ID is optional if the setting is not being
* set for a particular room, otherwise it should be supplied. The value may be null * set for a particular room, otherwise it should be supplied. The value may be null

View file

@ -23,7 +23,6 @@ limitations under the License.
* intended to handle environmental factors for specific settings. * intended to handle environmental factors for specific settings.
*/ */
export default class SettingController { export default class SettingController {
/** /**
* Gets the overridden value for the setting, if any. This must return null if the * 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. * 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. * the RoomList.
*/ */
class RoomListStore extends Store { class RoomListStore extends Store {
static _listOrders = { static _listOrders = {
"m.favourite": "manual", "m.favourite": "manual",
"im.vector.fake.invite": "recent", "im.vector.fake.invite": "recent",

View file

@ -93,7 +93,7 @@ export default class DMRoomMap {
return {userId, roomId}; return {userId, roomId};
} }
} }
}).filter((ids) => !!ids); //filter out }).filter((ids) => !!ids); //filter out
// these are actually all legit self-chats // these are actually all legit self-chats
// bail out // bail out
if (!guessedUserIdsThatChanged.length) { if (!guessedUserIdsThatChanged.length) {

View file

@ -77,7 +77,7 @@ const ALLOWED_BLOB_MIMETYPES = {
'audio/x-pn-wav': true, 'audio/x-pn-wav': true,
'audio/flac': true, 'audio/flac': true,
'audio/x-flac': true, 'audio/x-flac': true,
} };
/** /**
* Decrypt a file attached to a matrix event. * 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");
});
});