);
- }
+ },
});
diff --git a/src/components/views/globals/UpdateCheckBar.js b/src/components/views/globals/UpdateCheckBar.js
index e499ddab31..4610dbdb12 100644
--- a/src/components/views/globals/UpdateCheckBar.js
+++ b/src/components/views/globals/UpdateCheckBar.js
@@ -32,14 +32,14 @@ export default React.createClass({
getDefaultProps: function() {
return {
detail: '',
- }
+ };
},
getStatusText: function() {
// 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.
const updateCheckStatusEnum = PlatformPeg.get().getUpdateCheckStatusEnum();
- switch(this.props.status) {
+ switch (this.props.status) {
case updateCheckStatusEnum.ERROR:
return _t('Error encountered (%(errorDetail)s).', { errorDetail: this.props.detail });
case updateCheckStatusEnum.CHECKING:
@@ -59,7 +59,7 @@ export default React.createClass({
const message = this.getStatusText();
const warning = _t('Warning');
- if (!'getUpdateCheckStatusEnum' in PlatformPeg.get()) {
+ if (!('getUpdateCheckStatusEnum' in PlatformPeg.get())) {
return ;
}
@@ -83,9 +83,9 @@ export default React.createClass({
{message}
-
+
);
- }
+ },
});
diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js
index 6e0e5d538a..481cdc60b2 100644
--- a/src/components/views/login/InteractiveAuthEntryComponents.js
+++ b/src/components/views/login/InteractiveAuthEntryComponents.js
@@ -296,7 +296,7 @@ export const TermsAuthEntry = React.createClass({
return ;
}
- let checkboxes = [];
+ const checkboxes = [];
let allChecked = true;
for (const policy of this.state.policies) {
const checked = this.state.toggledPolicies[policy.id];
@@ -306,7 +306,7 @@ export const TermsAuthEntry = React.createClass({
+ ,
);
}
diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js
index 92a025f10c..de5d3db625 100644
--- a/src/components/views/room_settings/AliasSettings.js
+++ b/src/components/views/room_settings/AliasSettings.js
@@ -103,7 +103,7 @@ module.exports = React.createClass({
oldCanonicalAlias = this.props.canonicalAliasEvent.getContent().alias;
}
- let newCanonicalAlias = this.state.canonicalAlias;
+ const newCanonicalAlias = this.state.canonicalAlias;
if (this.props.canSetCanonicalAlias && oldCanonicalAlias !== newCanonicalAlias) {
console.log("AliasSettings: Updating canonical alias");
@@ -167,7 +167,7 @@ module.exports = React.createClass({
if (!this.props.canonicalAlias) {
this.setState({
- canonicalAlias: alias
+ canonicalAlias: alias,
});
}
},
diff --git a/src/components/views/room_settings/RelatedGroupSettings.js b/src/components/views/room_settings/RelatedGroupSettings.js
index 65132b9b74..4bad5ca806 100644
--- a/src/components/views/room_settings/RelatedGroupSettings.js
+++ b/src/components/views/room_settings/RelatedGroupSettings.js
@@ -22,7 +22,7 @@ import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import isEqual from 'lodash/isEqual';
-const GROUP_ID_REGEX = /\+\S+\:\S+/;
+const GROUP_ID_REGEX = /\+\S+:\S+/;
module.exports = React.createClass({
displayName: 'RelatedGroupSettings',
diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js
index 757204f0c8..e75456ea50 100644
--- a/src/components/views/rooms/Autocomplete.js
+++ b/src/components/views/rooms/Autocomplete.js
@@ -33,7 +33,6 @@ import Autocompleter from '../../../autocomplete/Autocompleter';
const COMPOSER_SELECTED = 0;
export default class Autocomplete extends React.Component {
-
constructor(props) {
super(props);
diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js
index 2c70f36e07..7f74176878 100644
--- a/src/components/views/rooms/LinkPreviewWidget.js
+++ b/src/components/views/rooms/LinkPreviewWidget.js
@@ -107,7 +107,7 @@ module.exports = React.createClass({
// FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing?
let image = p["og:image"];
- let imageMaxWidth = 100, imageMaxHeight = 100;
+ const imageMaxWidth = 100; const imageMaxHeight = 100;
if (image && image.startsWith("mxc://")) {
image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight);
}
diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js
index 037957c868..7ff52ecbb6 100644
--- a/src/components/views/rooms/MemberInfo.js
+++ b/src/components/views/rooms/MemberInfo.js
@@ -712,7 +712,7 @@ module.exports = withMatrixClient(React.createClass({
if (!member || !member.membership || member.membership === 'leave') {
const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId();
- const onInviteUserButton = async () => {
+ const onInviteUserButton = async() => {
try {
await cli.invite(roomId, member.userId);
} catch (err) {
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 66f3fdaa97..8df6a76836 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -269,7 +269,7 @@ export default class MessageComposer extends React.Component {
);
}
- let e2eImg, e2eTitle, e2eClass;
+ let e2eImg; let e2eTitle; let e2eClass;
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
if (roomIsEncrypted) {
// FIXME: show a /!\ if there are untrusted devices in the room...
@@ -429,7 +429,7 @@ export default class MessageComposer extends React.Component {
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
src="img/icon-text-cancel.svg" />
-
+ ;
}
return (
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js
index 570cb8a59b..14d394ab41 100644
--- a/src/components/views/rooms/MessageComposerInput.js
+++ b/src/components/views/rooms/MessageComposerInput.js
@@ -67,7 +67,7 @@ const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
const EMOJI_REGEX = new RegExp(unicodeRegexp, 'g');
-const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
+const TYPING_USER_TIMEOUT = 10000; const TYPING_SERVER_TIMEOUT = 30000;
const ENTITY_TYPES = {
AT_ROOM_PILL: 'ATROOMPILL',
@@ -175,8 +175,8 @@ export default class MessageComposerInput extends React.Component {
// see https://github.com/ianstormtaylor/slate/issues/762#issuecomment-304855095
this.direction = '';
- this.plainWithMdPills = new PlainWithPillsSerializer({ pillFormat: 'md' });
- this.plainWithIdPills = new PlainWithPillsSerializer({ pillFormat: 'id' });
+ this.plainWithMdPills = new PlainWithPillsSerializer({ pillFormat: 'md' });
+ this.plainWithIdPills = new PlainWithPillsSerializer({ pillFormat: 'id' });
this.plainWithPlainPills = new PlainWithPillsSerializer({ pillFormat: 'plain' });
this.md = new Md({
@@ -544,7 +544,7 @@ export default class MessageComposerInput extends React.Component {
if (editorState.startText !== null) {
const text = editorState.startText.text;
- const currentStartOffset = editorState.startOffset;
+ const currentStartOffset = editorState.selection.start.offset;
// Automatic replacement of plaintext emoji to Unicode emoji
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
@@ -558,11 +558,11 @@ export default class MessageComposerInput extends React.Component {
const range = Range.create({
anchor: {
- key: editorState.selection.startKey,
+ key: editorState.startText.key,
offset: currentStartOffset - emojiMatch[1].length - 1,
},
focus: {
- key: editorState.selection.startKey,
+ key: editorState.startText.key,
offset: currentStartOffset - 1,
},
});
@@ -1078,7 +1078,7 @@ export default class MessageComposerInput extends React.Component {
// only look for commands if the first block contains simple unformatted text
// i.e. no pills or rich-text formatting and begins with a /.
- let cmd, commandText;
+ let cmd; let commandText;
const firstChild = editorState.document.nodes.get(0);
const firstGrandChild = firstChild && firstChild.nodes.get(0);
if (firstChild && firstGrandChild &&
@@ -1260,7 +1260,7 @@ export default class MessageComposerInput extends React.Component {
}
};
- selectHistory = async (up) => {
+ selectHistory = async(up) => {
const delta = up ? -1 : 1;
// 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;
};
- onTab = async (e) => {
+ onTab = async(e) => {
this.setState({
someCompletions: null,
});
@@ -1330,7 +1330,7 @@ export default class MessageComposerInput extends React.Component {
up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow();
};
- onEscape = async (e) => {
+ onEscape = async(e) => {
e.preventDefault();
if (this.autocomplete) {
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 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;
if (displayedCompletion == null) {
diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js
index 9d3dbe5217..3c767b726a 100644
--- a/src/components/views/rooms/RoomPreviewBar.js
+++ b/src/components/views/rooms/RoomPreviewBar.js
@@ -93,7 +93,7 @@ module.exports = React.createClass({
},
render: function() {
- let joinBlock, previewBlock;
+ let joinBlock; let previewBlock;
if (this.props.spinner || this.state.busy) {
const Spinner = sdk.getComponent("elements.Spinner");
diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js
index b69938a117..8cd559f2ea 100644
--- a/src/components/views/rooms/RoomSettings.js
+++ b/src/components/views/rooms/RoomSettings.js
@@ -657,31 +657,31 @@ module.exports = React.createClass({
const userLevels = powerLevels.users || {};
const powerLevelDescriptors = {
- users_default: {
+ "users_default": {
desc: _t('The default role for new room members is'),
defaultValue: 0,
},
- events_default: {
+ "events_default": {
desc: _t('To send messages, you must be a'),
defaultValue: 0,
},
- invite: {
+ "invite": {
desc: _t('To invite users into the room, you must be a'),
defaultValue: 50,
},
- state_default: {
+ "state_default": {
desc: _t('To configure the room, you must be a'),
defaultValue: 50,
},
- kick: {
+ "kick": {
desc: _t('To kick users, you must be a'),
defaultValue: 50,
},
- ban: {
+ "ban": {
desc: _t('To ban users, you must be a'),
defaultValue: 50,
},
- redact: {
+ "redact": {
desc: _t('To remove other users\' messages, you must be a'),
defaultValue: 50,
},
diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js
index ea727a03b5..72ad2943aa 100644
--- a/src/components/views/settings/Notifications.js
+++ b/src/components/views/settings/Notifications.js
@@ -67,7 +67,7 @@ module.exports = React.createClass({
phases: {
LOADING: "LOADING", // The component is loading or sending data to the hs
DISPLAY: "DISPLAY", // The component is ready and display data
- ERROR: "ERROR", // There was an error
+ ERROR: "ERROR", // There was an error
},
propTypes: {
@@ -86,14 +86,14 @@ module.exports = React.createClass({
getInitialState: function() {
return {
phase: this.phases.LOADING,
- masterPushRule: undefined, // The master rule ('.m.rule.master')
- vectorPushRules: [], // HS default push rules displayed in Vector UI
- vectorContentRules: { // Keyword push rules displayed in Vector UI
+ masterPushRule: undefined, // The master rule ('.m.rule.master')
+ vectorPushRules: [], // HS default push rules displayed in Vector UI
+ vectorContentRules: { // Keyword push rules displayed in Vector UI
vectorState: PushRuleVectorState.ON,
rules: [],
},
- externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI
- externalContentRules: [], // Keyword push rules 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
};
},
@@ -290,7 +290,7 @@ module.exports = React.createClass({
for (const i in this.state.vectorContentRules.rules) {
const rule = this.state.vectorContentRules.rules[i];
- let enabled, actions;
+ let enabled; let actions;
switch (newPushRuleVectorState) {
case PushRuleVectorState.ON:
if (rule.actions.length !== 1) {
diff --git a/src/matrix-to.js b/src/matrix-to.js
index 90b0a66090..b5827f671a 100644
--- a/src/matrix-to.js
+++ b/src/matrix-to.js
@@ -14,11 +14,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import MatrixClientPeg from "./MatrixClientPeg";
+
export const host = "matrix.to";
export const baseUrl = `https://${host}`;
+// The maximum number of servers to pick when working out which servers
+// to add to permalinks. The servers are appended as ?via=example.org
+const MAX_SERVER_CANDIDATES = 3;
+
export function makeEventPermalink(roomId, eventId) {
- return `${baseUrl}/#/${roomId}/${eventId}`;
+ const permalinkBase = `${baseUrl}/#/${roomId}/${eventId}`;
+
+ // If the roomId isn't actually a room ID, don't try to list the servers.
+ // Aliases are already routable, and don't need extra information.
+ if (roomId[0] !== '!') return permalinkBase;
+
+ const serverCandidates = pickServerCandidates(roomId);
+ return `${permalinkBase}${encodeServerCandidates(serverCandidates)}`;
}
export function makeUserPermalink(userId) {
@@ -26,9 +39,98 @@ export function makeUserPermalink(userId) {
}
export function makeRoomPermalink(roomId) {
- return `${baseUrl}/#/${roomId}`;
+ const permalinkBase = `${baseUrl}/#/${roomId}`;
+
+ // If the roomId isn't actually a room ID, don't try to list the servers.
+ // Aliases are already routable, and don't need extra information.
+ if (roomId[0] !== '!') return permalinkBase;
+
+ const serverCandidates = pickServerCandidates(roomId);
+ return `${permalinkBase}${encodeServerCandidates(serverCandidates)}`;
}
export function makeGroupPermalink(groupId) {
return `${baseUrl}/#/${groupId}`;
}
+
+export function encodeServerCandidates(candidates) {
+ if (!candidates || candidates.length === 0) return '';
+ return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`;
+}
+
+export function pickServerCandidates(roomId) {
+ const client = MatrixClientPeg.get();
+ const room = client.getRoom(roomId);
+ if (!room) return [];
+
+ // Permalinks can have servers appended to them so that the user
+ // receiving them can have a fighting chance at joining the room.
+ // These servers are called "candidates" at this point because
+ // it is unclear whether they are going to be useful to actually
+ // join in the future.
+ //
+ // We pick 3 servers based on the following criteria:
+ //
+ // Server 1: The highest power level user in the room, provided
+ // they are at least PL 50. We don't calculate "what is a moderator"
+ // here because it is less relevant for the vast majority of rooms.
+ // We also want to ensure that we get an admin or high-ranking mod
+ // as they are less likely to leave the room. If no user happens
+ // to meet this criteria, we'll pick the most popular server in the
+ // room.
+ //
+ // Server 2: The next most popular server in the room (in user
+ // distribution). This cannot be the same as Server 1. If no other
+ // servers are available then we'll only return Server 1.
+ //
+ // Server 3: The next most popular server by user distribution. This
+ // has the same rules as Server 2, with the added exception that it
+ // must be unique from Server 1 and 2.
+
+ // Rationale for popular servers: It's hard to get rid of people when
+ // they keep flocking in from a particular server. Sure, the server could
+ // be ACL'd in the future or for some reason be evicted from the room
+ // however an event like that is unlikely the larger the room gets.
+
+ // Note: we don't pick the server the room was created on because the
+ // homeserver should already be using that server as a last ditch attempt
+ // and there's less of a guarantee that the server is a resident server.
+ // Instead, we actively figure out which servers are likely to be residents
+ // in the future and try to use those.
+
+ // Note: Users receiving permalinks that happen to have all 3 potential
+ // servers fail them (in terms of joining) are somewhat expected to hunt
+ // down the person who gave them the link to ask for a participating server.
+ // The receiving user can then manually append the known-good server to
+ // the list and magically have the link work.
+
+ const populationMap: {[server:string]:number} = {};
+ const highestPlUser = {userId: null, powerLevel: 0, serverName: null};
+
+ for (const member of room.getJoinedMembers()) {
+ const serverName = member.userId.split(":").splice(1).join(":");
+ if (member.powerLevel > highestPlUser.powerLevel) {
+ highestPlUser.userId = member.userId;
+ highestPlUser.powerLevel = member.powerLevel;
+ highestPlUser.serverName = serverName;
+ }
+
+ if (!populationMap[serverName]) populationMap[serverName] = 0;
+ populationMap[serverName]++;
+ }
+
+ const candidates = [];
+ if (highestPlUser.powerLevel >= 50) candidates.push(highestPlUser.serverName);
+
+ const beforePopulation = candidates.length;
+ const serversByPopulation = Object.keys(populationMap)
+ .sort((a, b) => populationMap[b] - populationMap[a])
+ .filter(a => !candidates.includes(a));
+ for (let i = beforePopulation; i <= MAX_SERVER_CANDIDATES; i++) {
+ const idx = i - beforePopulation;
+ if (idx >= serversByPopulation.length) break;
+ candidates.push(serversByPopulation[idx]);
+ }
+
+ return candidates;
+}
diff --git a/src/notifications/StandardActions.js b/src/notifications/StandardActions.js
index 22a8f1db40..30d6ea5975 100644
--- a/src/notifications/StandardActions.js
+++ b/src/notifications/StandardActions.js
@@ -16,9 +16,9 @@ limitations under the License.
'use strict';
-var NotificationUtils = require('./NotificationUtils');
+const NotificationUtils = require('./NotificationUtils');
-var encodeActions = NotificationUtils.encodeActions;
+const encodeActions = NotificationUtils.encodeActions;
module.exports = {
ACTION_NOTIFY: encodeActions({notify: true}),
diff --git a/src/phonenumber.js b/src/phonenumber.js
index aaf018ba26..3b7e4ba4f1 100644
--- a/src/phonenumber.js
+++ b/src/phonenumber.js
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-const PHONE_NUMBER_REGEXP = /^[0-9 -\.]+$/;
+const PHONE_NUMBER_REGEXP = /^[0-9 -.]+$/;
/*
* Do basic validation to determine if the given input could be
diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js
index cb6d83e884..bb3f412a9b 100644
--- a/src/settings/SettingsStore.js
+++ b/src/settings/SettingsStore.js
@@ -248,7 +248,7 @@ export default class SettingsStore {
if (actualValue !== undefined && actualValue !== null) return actualValue;
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
* set for a particular room, otherwise it should be supplied. The value may be null
diff --git a/src/settings/controllers/SettingController.js b/src/settings/controllers/SettingController.js
index 0ebe0042e6..a7d0ccf21a 100644
--- a/src/settings/controllers/SettingController.js
+++ b/src/settings/controllers/SettingController.js
@@ -23,7 +23,6 @@ limitations under the License.
* intended to handle environmental factors for specific settings.
*/
export default class SettingController {
-
/**
* Gets the overridden value for the setting, if any. This must return null if the
* value is not to be overridden, otherwise it must return the new value.
diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js
index 67c0c13be7..c670161dbc 100644
--- a/src/stores/RoomListStore.js
+++ b/src/stores/RoomListStore.js
@@ -23,7 +23,6 @@ import Unread from '../Unread';
* the RoomList.
*/
class RoomListStore extends Store {
-
static _listOrders = {
"m.favourite": "manual",
"im.vector.fake.invite": "recent",
diff --git a/src/utils/DMRoomMap.js b/src/utils/DMRoomMap.js
index bb5e4d706b..af65b6f001 100644
--- a/src/utils/DMRoomMap.js
+++ b/src/utils/DMRoomMap.js
@@ -93,7 +93,7 @@ export default class DMRoomMap {
return {userId, roomId};
}
}
- }).filter((ids) => !!ids); //filter out
+ }).filter((ids) => !!ids); //filter out
// these are actually all legit self-chats
// bail out
if (!guessedUserIdsThatChanged.length) {
diff --git a/src/utils/DecryptFile.js b/src/utils/DecryptFile.js
index 92c2e3644d..ea0e4c3fb0 100644
--- a/src/utils/DecryptFile.js
+++ b/src/utils/DecryptFile.js
@@ -77,7 +77,7 @@ const ALLOWED_BLOB_MIMETYPES = {
'audio/x-pn-wav': true,
'audio/flac': true,
'audio/x-flac': true,
-}
+};
/**
* Decrypt a file attached to a matrix event.
diff --git a/test/matrix-to-test.js b/test/matrix-to-test.js
new file mode 100644
index 0000000000..70533575c4
--- /dev/null
+++ b/test/matrix-to-test.js
@@ -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");
+ });
+});