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