Merge remote-tracking branch 'origin/develop' into fix_riot_web_4821

This commit is contained in:
nostradamos 2018-01-02 23:07:47 +01:00
commit 36df3acf4d
137 changed files with 5949 additions and 2020 deletions

View file

@ -29,6 +29,10 @@ module.exports = {
// so we replace it with a version that is class property aware // so we replace it with a version that is class property aware
"babel/no-invalid-this": "error", "babel/no-invalid-this": "error",
// We appear to follow this most of the time, so let's enforce it instead
// of occasionally following it (or catching it in review)
"keyword-spacing": "error",
/** react **/ /** react **/
// This just uses the react plugin to help eslint known when // This just uses the react plugin to help eslint known when
// variables have been used in JSX // variables have been used in JSX

151
docs/settings.md Normal file
View file

@ -0,0 +1,151 @@
# Settings Reference
This document serves as developer documentation for using "Granular Settings". Granular Settings allow users to specify different values for a setting at particular levels of interest. For example, a user may say that in a particular room they want URL previews off, but in all other rooms they want them enabled. The `SettingsStore` helps mask the complexity of dealing with the different levels and exposes easy to use getters and setters.
## Levels
Granular Settings rely on a series of known levels in order to use the correct value for the scenario. These levels, in order of prioirty, are:
* `device` - The current user's device
* `room-device` - The current user's device, but only when in a specific room
* `room-account` - The current user's account, but only when in a specific room
* `account` - The current user's account
* `room` - A specific room (setting for all members of the room)
* `config` - Values are defined by `config.json`
* `default` - The hardcoded default for the settings
Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure that room administrators cannot force account-only settings upon participants.
## Settings
Settings are the different options a user may set or experience in the application. These are pre-defined in `src/settings/Settings.js` under the `SETTINGS` constant and have the following minimum requirements:
```
// The ID is used to reference the setting throughout the application. This must be unique.
"theSettingId": {
// The levels this setting supports is required. In `src/settings/Settings.js` there are various pre-set arrays
// for this option - they should be used where possible to avoid copy/pasting arrays across settings.
supportedLevels: [...],
// The default for this setting serves two purposes: It provides a value if the setting is not defined at other
// levels, and it serves to demonstrate the expected type to other developers. The value isn't enforced, but it
// should be respected throughout the code. The default may be any data type.
default: false,
// The display name has two notations: string and object. The object notation allows for different translatable
// strings to be used for different levels, while the string notation represents the string for all levels.
displayName: _td("Change something"), // effectively `displayName: { "default": _td("Change something") }`
displayName: {
"room": _td("Change something for participants of this room"),
// Note: the default will be used if the level requested (such as `device`) does not have a string defined here.
"default": _td("Change something"),
}
}
```
### Getting values for a setting
After importing `SettingsStore`, simply make a call to `SettingsStore.getValue`. The `roomId` parameter should always be supplied where possible, even if the setting does not have a per-room level value. This is to ensure that the value returned is best represented in the room, particularly if the setting ever gets a per-room level in the future.
In settings pages it is often desired to have the value at a particular level instead of getting the calculated value. Call `SettingsStore.getValueAt` to get the value of a setting at a particular level, and optionally make it explicitly at that level. By default `getValueAt` will traverse the tree starting at the provided level; making it explicit means it will not go beyond the provided level. When using `getValueAt`, please be sure to use `SettingLevel` to represent the target level.
### Setting values for a setting
Values are defined at particular levels and should be done in a safe manner. There are two checks to perform to ensure a clean save: is the level supported and can the user actually set the value. In most cases, neither should be an issue although there are circumstances where this changes. An example of a safe call is:
```javascript
const isSupported = SettingsStore.isLevelSupported(SettingLevel.ROOM);
if (isSupported) {
const canSetValue = SettingsStore.canSetValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM);
if (canSetValue) {
SettingsStore.setValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM, newValue);
}
}
```
These checks may also be performed in different areas of the application to avoid the verbose example above. For instance, the component which allows changing the setting may be hidden conditionally on the above conditions.
##### `SettingsFlag` component
Where possible, the `SettingsFlag` component should be used to set simple "flip-a-bit" (true/false) settings. The `SettingsFlag` also supports simple radio button options, such as the theme the user would like to use.
```html
<SettingsFlag name="theSettingId"
level={SettingsLevel.ROOM}
roomId="!curbf:matrix.org"
label={_td("Your label here")} // optional, if falsey then the `SettingsStore` will be used
onChange={function(newValue) { }} // optional, called after saving
isExplicit={false} // this is passed along to `SettingsStore.getValueAt`, defaulting to false
manualSave={false} // if true, saving is delayed. You will need to call .save() on this component
// Options for radio buttons
group="your-radio-group" // this enables radio button support
value="yourValueHere" // the value for this particular option
/>
```
### Getting the display name for a setting
Simply call `SettingsStore.getDisplayName`. The appropriate display name will be returned and automatically translated for you. If a display name cannot be found, it will return `null`.
## Features
Occasionally some parts of the application may be undergoing testing and are not quite production ready. These are commonly known to be behind a "labs flag". Features behind lab flags must go through the granular settings system, and look and act very much normal settings. The exception is that they must supply `isFeature: true` as part of the setting definition and should go through the helper functions on `SettingsStore`.
### Determining if a feature is enabled
A simple call to `SettingsStore.isFeatureEnabled` will tell you if the feature is enabled. This will perform all the required calculations to determine if the feature is enabled based upon the configuration and user selection.
### Enabling a feature
Features can only be enabled if the feature is in the `labs` state, otherwise this is a no-op. To find the current set of features in the `labs` state, call `SettingsStore.getLabsFeatures`. To set the value, call `SettingsStore.setFeatureEnabled`.
## Setting controllers
Settings may have environmental factors that affect their value or need additional code to be called when they are modified. A setting controller is able to override the calculated value for a setting and react to changes in that setting. Controllers are not a replacement for the level handlers and should only be used to ensure the environment is kept up to date with the setting where it is otherwise not possible. An example of this is the notification settings: they can only be considered enabled if the platform supports notifications, and enabling notifications requires additional steps to actually enable notifications.
For more information, see `src/settings/controllers/SettingController.js`.
## Local echo
`SettingsStore` will perform local echo on all settings to ensure that immediately getting values does not cause a split-brain scenario. As mentioned in the "Setting values for a setting" section, the appropriate checks should be done to ensure that the user is allowed to set the value. The local echo system assumes that the user has permission and that the request will go through successfully. The local echo only takes effect until the request to save a setting has completed (either successfully or otherwise).
```javascript
SettingsStore.setValue(...).then(() => {
// The value has actually been stored at this point.
});
SettingsStore.getValue(...); // this will return the value set in `setValue` above.
```
# Maintainers Reference
The granular settings system has a few complex parts to power it. This section is to document how the `SettingsStore` is supposed to work.
### General information
The `SettingsStore` uses the hardcoded `LEVEL_ORDER` constant to ensure that it is using the correct override procedure. The array is checked from left to right, simulating the behaviour of overriding values from the higher levels. Each level should be defined in this array, including `default`.
Handlers (`src/settings/handlers/SettingsHandler.js`) represent a single level and are responsible for getting and setting values at that level. Handlers also provide additional information to the `SettingsStore` such as if the level is supported or if the current user may set values at the level. The `SettingsStore` will use the handler to enforce checks and manipulate settings. Handlers are also responsible for dealing with migration patterns or legacy settings for their level (for example, a setting being renamed or using a different key from other settings in the underlying store). Handlers are provided to the `SettingsStore` via the `LEVEL_HANDLERS` constant. `SettingsStore` will optimize lookups by only considering handlers that are supported on the platform.
Local echo is achieved through `src/settings/handlers/LocalEchoWrapper.js` which acts as a wrapper around a given handler. This is automatically applied to all defined `LEVEL_HANDLERS` and proxies the calls to the wrapped handler where possible. The echo is achieved by a simple object cache stored within the class itself. The cache is invalidated immediately upon the proxied save call succeeding or failing.
Controllers are notified of changes by the `SettingsStore`, and are given the opportunity to override values after the `SettingsStore` has deemed the value calculated. Controllers are invoked as the last possible step in the code.
### Features
Features automatically get considered as `disabled` if they are not listed in the `SdkConfig` or `enable_labs` is false/not set. Features are always checked against the configuration before going through the level order as they have the option of being forced-on or forced-off for the application. This is done by the `features` section and looks something like this:
```
"features": {
"feature_groups": "enable",
"feature_pinning": "disable", // the default
"feature_presence": "labs"
}
```
If `enableLabs` is true in the configuration, the default for features becomes `"labs"`.

View file

@ -77,6 +77,8 @@
"querystring": "^0.2.0", "querystring": "^0.2.0",
"react": "^15.4.0", "react": "^15.4.0",
"react-addons-css-transition-group": "15.3.2", "react-addons-css-transition-group": "15.3.2",
"react-dnd": "^2.1.4",
"react-dnd-html5-backend": "^2.1.2",
"react-dom": "^15.4.0", "react-dom": "^15.4.0",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
"sanitize-html": "^1.14.1", "sanitize-html": "^1.14.1",

View file

@ -32,7 +32,7 @@ const walk = require('walk');
const flowParser = require('flow-parser'); const flowParser = require('flow-parser');
const estreeWalker = require('estree-walker'); const estreeWalker = require('estree-walker');
const TRANSLATIONS_FUNCS = ['_t', '_td', '_tJsx']; const TRANSLATIONS_FUNCS = ['_t', '_td'];
const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json'; const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json';
const OUTPUT_FILE = 'src/i18n/strings/en_EN.json'; const OUTPUT_FILE = 'src/i18n/strings/en_EN.json';
@ -126,7 +126,7 @@ function getTranslationsJs(file) {
if (tKey === null) return; if (tKey === null) return;
// check the format string against the args // check the format string against the args
// We only check _t: _tJsx is much more complex and _td has no args // We only check _t: _td has no args
if (node.callee.name === '_t') { if (node.callee.name === '_t') {
try { try {
const placeholders = getFormatStrings(tKey); const placeholders = getFormatStrings(tKey);
@ -139,6 +139,22 @@ function getTranslationsJs(file) {
throw new Error(`No value found for placeholder '${placeholder}'`); throw new Error(`No value found for placeholder '${placeholder}'`);
} }
} }
// Validate tag replacements
if (node.arguments.length > 2) {
const tagMap = node.arguments[2];
for (const prop of tagMap.properties) {
if (prop.key.type === 'Literal') {
const tag = prop.key.value;
// RegExp same as in src/languageHandler.js
const regexp = new RegExp(`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`);
if (!tKey.match(regexp)) {
throw new Error(`No match for ${regexp} in ${tKey}`);
}
}
}
}
} catch (e) { } catch (e) {
console.log(); console.log();
console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`); console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`);

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 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.
@ -58,6 +59,7 @@ import sdk from './index';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import dis from './dispatcher'; import dis from './dispatcher';
import { showUnknownDeviceDialogForCalls } from './cryptodevices';
global.mxCalls = { global.mxCalls = {
//room_id: MatrixCall //room_id: MatrixCall
@ -97,19 +99,54 @@ function pause(audioId) {
} }
} }
function _reAttemptCall(call) {
if (call.direction === 'outbound') {
dis.dispatch({
action: 'place_call',
room_id: call.roomId,
type: call.type,
});
} else {
call.answer();
}
}
function _setCallListeners(call) { function _setCallListeners(call) {
call.on("error", function(err) { call.on("error", function(err) {
console.error("Call error: %s", err); console.error("Call error: %s", err);
console.error(err.stack); console.error(err.stack);
call.hangup(); if (err.code === 'unknown_devices') {
_setCallState(undefined, call.roomId, "ended"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Call Failed', '', QuestionDialog, {
title: _t('Call Failed'),
description: _t(
"There are unknown devices in this room: "+
"if you proceed without verifying them, it will be "+
"possible for someone to eavesdrop on your call."
),
button: _t('Review Devices'),
onFinished: function(confirmed) {
if (confirmed) {
const room = MatrixClientPeg.get().getRoom(call.roomId);
showUnknownDeviceDialogForCalls(
MatrixClientPeg.get(),
room,
() => {
_reAttemptCall(call);
},
call.direction === 'outbound' ? _t("Call Anyway") : _t("Answer Anyway"),
call.direction === 'outbound' ? _t("Call") : _t("Answer"),
);
}
},
}); });
call.on('send_event_error', function(err) { } else {
if (err.name === "UnknownDeviceError") { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
dis.dispatch({
action: 'unknown_device_error', Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
err: err, title: _t('Call Failed'),
room: MatrixClientPeg.get().getRoom(call.roomId), description: err.message,
}); });
} }
}); });
@ -179,7 +216,6 @@ function _setCallState(call, roomId, status) {
function _onAction(payload) { function _onAction(payload) {
function placeCall(newCall) { function placeCall(newCall) {
_setCallListeners(newCall); _setCallListeners(newCall);
_setCallState(newCall, newCall.roomId, "ringback");
if (payload.type === 'voice') { if (payload.type === 'voice') {
newCall.placeVoiceCall(); newCall.placeVoiceCall();
} else if (payload.type === 'video') { } else if (payload.type === 'video') {

View file

@ -14,8 +14,8 @@
limitations under the License. limitations under the License.
*/ */
import UserSettingsStore from './UserSettingsStore';
import * as Matrix from 'matrix-js-sdk'; import * as Matrix from 'matrix-js-sdk';
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
export default { export default {
getDevices: function() { getDevices: function() {
@ -43,22 +43,20 @@ export default {
}, },
loadDevices: function() { loadDevices: function() {
// this.getDevices().then((devices) => { const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
const localSettings = UserSettingsStore.getLocalSettings(); const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
// // if deviceId is not found, automatic fallback is in spec
// // recall previously stored inputs if any Matrix.setMatrixCallAudioInput(audioDeviceId);
Matrix.setMatrixCallAudioInput(localSettings['webrtc_audioinput']); Matrix.setMatrixCallVideoInput(videoDeviceId);
Matrix.setMatrixCallVideoInput(localSettings['webrtc_videoinput']);
// });
}, },
setAudioInput: function(deviceId) { setAudioInput: function(deviceId) {
UserSettingsStore.setLocalSetting('webrtc_audioinput', deviceId); SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
Matrix.setMatrixCallAudioInput(deviceId); Matrix.setMatrixCallAudioInput(deviceId);
}, },
setVideoInput: function(deviceId) { setVideoInput: function(deviceId) {
UserSettingsStore.setLocalSetting('webrtc_videoinput', deviceId); SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
Matrix.setMatrixCallVideoInput(deviceId); Matrix.setMatrixCallVideoInput(deviceId);
}, },
}; };

View file

@ -114,7 +114,7 @@ function _onGroupInviteFinished(groupId, addrs) {
function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) { function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
const matrixClient = MatrixClientPeg.get(); const matrixClient = MatrixClientPeg.get();
const groupStore = GroupStoreCache.getGroupStore(matrixClient, groupId); const groupStore = GroupStoreCache.getGroupStore(groupId);
const errorList = []; const errorList = [];
return Promise.all(addrs.map((addr) => { return Promise.all(addrs.map((addr) => {
return groupStore return groupStore

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2017 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.
@ -15,7 +16,7 @@ limitations under the License.
*/ */
/* a selection of key codes, as used in KeyboardEvent.keyCode */ /* a selection of key codes, as used in KeyboardEvent.keyCode */
module.exports = { export const KeyCode = {
BACKSPACE: 8, BACKSPACE: 8,
TAB: 9, TAB: 9,
ENTER: 13, ENTER: 13,
@ -58,3 +59,12 @@ module.exports = {
KEY_Y: 89, KEY_Y: 89,
KEY_Z: 90, KEY_Z: 90,
}; };
export function isOnlyCtrlOrCmdKeyEvent(ev) {
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
if (isMac) {
return ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
} else {
return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
}
}

View file

@ -389,6 +389,8 @@ function _persistCredentialsToLocalStorage(credentials) {
* Logs the current session out and transitions to the logged-out state * Logs the current session out and transitions to the logged-out state
*/ */
export function logout() { export function logout() {
if (!MatrixClientPeg.get()) return;
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
// logout doesn't work for guest sessions // logout doesn't work for guest sessions
// Also we sometimes want to re-log in a guest session // Also we sometimes want to re-log in a guest session
@ -436,6 +438,10 @@ function startMatrixClient() {
DMRoomMap.makeShared().start(); DMRoomMap.makeShared().start();
MatrixClientPeg.start(); MatrixClientPeg.start();
// dispatch that we finished starting up to wire up any other bits
// of the matrix client that cannot be set prior to starting up.
dis.dispatch({action: 'client_started'});
} }
/* /*

View file

@ -204,6 +204,12 @@ export default class Login {
} }
throw originalLoginError; throw originalLoginError;
}).catch((error) => { }).catch((error) => {
// We apparently squash case at login serverside these days:
// https://github.com/matrix-org/synapse/blob/1189be43a2479f5adf034613e8d10e3f4f452eb9/synapse/handlers/auth.py#L475
// so this wasn't needed after all. Keeping the code around in case the
// the situation changes...
/*
if ( if (
error.httpStatus === 403 && error.httpStatus === 403 &&
loginParams.identifier.type === 'm.id.user' && loginParams.identifier.type === 'm.id.user' &&
@ -211,6 +217,7 @@ export default class Login {
) { ) {
return tryLowercaseUsername(originalLoginError); return tryLowercaseUsername(originalLoginError);
} }
*/
throw originalLoginError; throw originalLoginError;
}).catch((error) => { }).catch((error) => {
console.log("Login failed", error); console.log("Login failed", error);

View file

@ -1,6 +1,7 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd. Copyright 2017 Vector Creations Ltd.
Copyright 2017 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.
@ -21,6 +22,8 @@ import utils from 'matrix-js-sdk/lib/utils';
import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline'; import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline';
import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set'; import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set';
import createMatrixClient from './utils/createMatrixClient'; import createMatrixClient from './utils/createMatrixClient';
import SettingsStore from './settings/SettingsStore';
import MatrixActionCreators from './actions/MatrixActionCreators';
interface MatrixClientCreds { interface MatrixClientCreds {
homeserverUrl: string, homeserverUrl: string,
@ -67,6 +70,8 @@ class MatrixClientPeg {
unset() { unset() {
this.matrixClient = null; this.matrixClient = null;
MatrixActionCreators.stop();
} }
/** /**
@ -93,6 +98,7 @@ class MatrixClientPeg {
const opts = utils.deepCopy(this.opts); const opts = utils.deepCopy(this.opts);
// the react sdk doesn't work without this, so don't allow // the react sdk doesn't work without this, so don't allow
opts.pendingEventOrdering = "detached"; opts.pendingEventOrdering = "detached";
opts.disablePresence = true; // we do this manually
try { try {
const promise = this.matrixClient.store.startup(); const promise = this.matrixClient.store.startup();
@ -106,6 +112,9 @@ class MatrixClientPeg {
// regardless of errors, start the client. If we did error out, we'll // regardless of errors, start the client. If we did error out, we'll
// just end up doing a full initial /sync. // just end up doing a full initial /sync.
// Connect the matrix client to the dispatcher
MatrixActionCreators.start(this.matrixClient);
console.log(`MatrixClientPeg: really starting MatrixClient`); console.log(`MatrixClientPeg: really starting MatrixClient`);
this.get().startClient(opts); this.get().startClient(opts);
console.log(`MatrixClientPeg: MatrixClient started`); console.log(`MatrixClientPeg: MatrixClient started`);
@ -136,9 +145,6 @@ class MatrixClientPeg {
} }
_createClient(creds: MatrixClientCreds) { _createClient(creds: MatrixClientCreds) {
// XXX: This is here and as a require because apparently circular dependencies
// are just broken in webpack (https://github.com/webpack/webpack/issues/1788)
const UserSettingsStore = require('./UserSettingsStore');
const opts = { const opts = {
baseUrl: creds.homeserverUrl, baseUrl: creds.homeserverUrl,
idBaseUrl: creds.identityServerUrl, idBaseUrl: creds.identityServerUrl,
@ -146,7 +152,7 @@ class MatrixClientPeg {
userId: creds.userId, userId: creds.userId,
deviceId: creds.deviceId, deviceId: creds.deviceId,
timelineSupport: true, timelineSupport: true,
forceTURN: UserSettingsStore.getLocalSetting('webRtcForceTURN', false), forceTURN: SettingsStore.getValue('webRtcForceTURN', false),
}; };
this.matrixClient = createMatrixClient(opts, this.indexedDbWorkerScript); this.matrixClient = createMatrixClient(opts, this.indexedDbWorkerScript);

View file

@ -25,6 +25,7 @@ import dis from './dispatcher';
import sdk from './index'; import sdk from './index';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import Modal from './Modal'; import Modal from './Modal';
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
/* /*
* Dispatches: * Dispatches:
@ -138,10 +139,8 @@ const Notifier = {
// make sure that we persist the current setting audio_enabled setting // make sure that we persist the current setting audio_enabled setting
// before changing anything // before changing anything
if (global.localStorage) { if (SettingsStore.isLevelSupported(SettingLevel.DEVICE)) {
if (global.localStorage.getItem('audio_notifications_enabled') === null) { SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, this.isEnabled());
this.setAudioEnabled(this.isEnabled());
}
} }
if (enable) { if (enable) {
@ -149,6 +148,7 @@ const Notifier = {
plaf.requestNotificationPermission().done((result) => { plaf.requestNotificationPermission().done((result) => {
if (result !== 'granted') { if (result !== 'granted') {
// The permission request was dismissed or denied // The permission request was dismissed or denied
// TODO: Support alternative branding in messaging
const description = result === 'denied' const description = result === 'denied'
? _t('Riot does not have permission to send you notifications - please check your browser settings') ? _t('Riot does not have permission to send you notifications - please check your browser settings')
: _t('Riot was not given permission to send notifications - please try again'); : _t('Riot was not given permission to send notifications - please try again');
@ -160,10 +160,6 @@ const Notifier = {
return; return;
} }
if (global.localStorage) {
global.localStorage.setItem('notifications_enabled', 'true');
}
if (callback) callback(); if (callback) callback();
dis.dispatch({ dis.dispatch({
action: "notifier_enabled", action: "notifier_enabled",
@ -174,8 +170,6 @@ const Notifier = {
// disabled again in the future, we will show the banner again. // disabled again in the future, we will show the banner again.
this.setToolbarHidden(false); this.setToolbarHidden(false);
} else { } else {
if (!global.localStorage) return;
global.localStorage.setItem('notifications_enabled', 'false');
dis.dispatch({ dis.dispatch({
action: "notifier_enabled", action: "notifier_enabled",
value: false, value: false,
@ -184,44 +178,24 @@ const Notifier = {
}, },
isEnabled: function() { isEnabled: function() {
return this.isPossible() && SettingsStore.getValue("notificationsEnabled");
},
isPossible: function() {
const plaf = PlatformPeg.get(); const plaf = PlatformPeg.get();
if (!plaf) return false; if (!plaf) return false;
if (!plaf.supportsNotifications()) return false; if (!plaf.supportsNotifications()) return false;
if (!plaf.maySendNotifications()) return false; if (!plaf.maySendNotifications()) return false;
if (!global.localStorage) return true; return true; // possible, but not necessarily enabled
const enabled = global.localStorage.getItem('notifications_enabled');
if (enabled === null) return true;
return enabled === 'true';
},
setBodyEnabled: function(enable) {
if (!global.localStorage) return;
global.localStorage.setItem('notifications_body_enabled', enable ? 'true' : 'false');
}, },
isBodyEnabled: function() { isBodyEnabled: function() {
if (!global.localStorage) return true; return this.isEnabled() && SettingsStore.getValue("notificationBodyEnabled");
const enabled = global.localStorage.getItem('notifications_body_enabled');
// default to true if the popups are enabled
if (enabled === null) return this.isEnabled();
return enabled === 'true';
}, },
setAudioEnabled: function(enable) { isAudioEnabled: function() {
if (!global.localStorage) return; return this.isEnabled() && SettingsStore.getValue("audioNotificationsEnabled");
global.localStorage.setItem('audio_notifications_enabled',
enable ? 'true' : 'false');
},
isAudioEnabled: function(enable) {
if (!global.localStorage) return true;
const enabled = global.localStorage.getItem(
'audio_notifications_enabled');
// default to true if the popups are enabled
if (enabled === null) return this.isEnabled();
return enabled === 'true';
}, },
setToolbarHidden: function(hidden, persistent = true) { setToolbarHidden: function(hidden, persistent = true) {
@ -238,16 +212,14 @@ const Notifier = {
// update the info to localStorage for persistent settings // update the info to localStorage for persistent settings
if (persistent && global.localStorage) { if (persistent && global.localStorage) {
global.localStorage.setItem('notifications_hidden', hidden); global.localStorage.setItem("notifications_hidden", hidden);
} }
}, },
isToolbarHidden: function() { isToolbarHidden: function() {
// Check localStorage for any such meta data // Check localStorage for any such meta data
if (global.localStorage) { if (global.localStorage) {
if (global.localStorage.getItem('notifications_hidden') === 'true') { return global.localStorage.getItem("notifications_hidden") === "true";
return true;
}
} }
return this.toolbarHidden; return this.toolbarHidden;

View file

@ -56,13 +56,27 @@ class Presence {
return this.state; return this.state;
} }
/**
* Get the current status message.
* @returns {String} the status message, may be null
*/
getStatusMessage() {
return this.statusMessage;
}
/** /**
* Set the presence state. * Set the presence state.
* If the state has changed, the Home Server will be notified. * If the state has changed, the Home Server will be notified.
* @param {string} newState the new presence state (see PRESENCE enum) * @param {string} newState the new presence state (see PRESENCE enum)
* @param {String} statusMessage an optional status message for the presence
* @param {boolean} maintain true to have this status maintained by this tracker
*/ */
setState(newState) { setState(newState, statusMessage=null, maintain=false) {
if (newState === this.state) { if (this.maintain) {
// Don't update presence if we're maintaining a particular status
return;
}
if (newState === this.state && statusMessage === this.statusMessage) {
return; return;
} }
if (PRESENCE_STATES.indexOf(newState) === -1) { if (PRESENCE_STATES.indexOf(newState) === -1) {
@ -72,21 +86,37 @@ class Presence {
return; return;
} }
const old_state = this.state; const old_state = this.state;
const old_message = this.statusMessage;
this.state = newState; this.state = newState;
this.statusMessage = statusMessage;
this.maintain = maintain;
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
return; // don't try to set presence when a guest; it won't work. return; // don't try to set presence when a guest; it won't work.
} }
const updateContent = {
presence: this.state,
status_msg: this.statusMessage ? this.statusMessage : '',
};
const self = this; const self = this;
MatrixClientPeg.get().setPresence(this.state).done(function() { MatrixClientPeg.get().setPresence(updateContent).done(function() {
console.log("Presence: %s", newState); console.log("Presence: %s", newState);
// We have to dispatch because the js-sdk is unreliable at telling us about our own presence
dis.dispatch({action: "self_presence_updated", statusInfo: updateContent});
}, function(err) { }, function(err) {
console.error("Failed to set presence: %s", err); console.error("Failed to set presence: %s", err);
self.state = old_state; self.state = old_state;
self.statusMessage = old_message;
}); });
} }
stopMaintainingStatus() {
this.maintain = false;
}
/** /**
* Callback called when the user made no action on the page for UNAVAILABLE_TIME ms. * Callback called when the user made no action on the page for UNAVAILABLE_TIME ms.
* @private * @private
@ -95,7 +125,8 @@ class Presence {
this.setState("unavailable"); this.setState("unavailable");
} }
_onUserActivity() { _onUserActivity(payload) {
if (payload.action === "sync_state" || payload.action === "self_presence_updated") return;
this._resetTimer(); this._resetTimer();
} }

View file

@ -44,13 +44,6 @@ module.exports = {
// XXX: temporary logging to try to diagnose // XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148 // https://github.com/vector-im/riot-web/issues/3148
console.log('Resend got send failure: ' + err.name + '('+err+')'); console.log('Resend got send failure: ' + err.name + '('+err+')');
if (err.name === "UnknownDeviceError") {
dis.dispatch({
action: 'unknown_device_error',
err: err,
room: room,
});
}
dis.dispatch({ dis.dispatch({
action: 'message_send_failed', action: 'message_send_failed',
@ -60,9 +53,5 @@ module.exports = {
}, },
removeFromQueue: function(event) { removeFromQueue: function(event) {
MatrixClientPeg.get().cancelPendingEvent(event); MatrixClientPeg.get().cancelPendingEvent(event);
dis.dispatch({
action: 'message_send_cancelled',
event: event,
});
}, },
}; };

View file

@ -15,19 +15,20 @@ limitations under the License.
*/ */
import { _t } from './languageHandler'; import { _t } from './languageHandler';
export function levelRoleMap() { export function levelRoleMap(usersDefault) {
return { return {
undefined: _t('Default'), undefined: _t('Default'),
0: _t('User'), 0: _t('Restricted'),
[usersDefault]: _t('Default'),
50: _t('Moderator'), 50: _t('Moderator'),
100: _t('Admin'), 100: _t('Admin'),
}; };
} }
export function textualPowerLevel(level, userDefault) { export function textualPowerLevel(level, usersDefault) {
const LEVEL_ROLE_MAP = this.levelRoleMap(); const LEVEL_ROLE_MAP = this.levelRoleMap(usersDefault);
if (LEVEL_ROLE_MAP[level]) { if (LEVEL_ROLE_MAP[level]) {
return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`); return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${usersDefault})`);
} else { } else {
return level; return level;
} }

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import Promise from 'bluebird'; import Promise from 'bluebird';
import SettingsStore from "./settings/SettingsStore";
const request = require('browser-request'); const request = require('browser-request');
const SdkConfig = require('./SdkConfig'); const SdkConfig = require('./SdkConfig');
@ -76,10 +77,40 @@ class ScalarAuthClient {
return defer.promise; return defer.promise;
} }
getScalarPageTitle(url) {
const defer = Promise.defer();
let scalarPageLookupUrl = SdkConfig.get().integrations_rest_url + '/widgets/title_lookup';
scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl);
scalarPageLookupUrl += '&curl=' + encodeURIComponent(url);
request({
method: 'GET',
uri: scalarPageLookupUrl,
json: true,
}, (err, response, body) => {
if (err) {
defer.reject(err);
} else if (response.statusCode / 100 !== 2) {
defer.reject({statusCode: response.statusCode});
} else if (!body) {
defer.reject(new Error("Missing page title in response"));
} else {
let title = "";
if (body.page_title_cache_item && body.page_title_cache_item.cached_title) {
title = body.page_title_cache_item.cached_title;
}
defer.resolve(title);
}
});
return defer.promise;
}
getScalarInterfaceUrlForRoom(roomId, screen, id) { getScalarInterfaceUrlForRoom(roomId, screen, id) {
let url = SdkConfig.get().integrations_ui_url; let url = SdkConfig.get().integrations_ui_url;
url += "?scalar_token=" + encodeURIComponent(this.scalarToken); url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
url += "&room_id=" + encodeURIComponent(roomId); url += "&room_id=" + encodeURIComponent(roomId);
url += "&theme=" + encodeURIComponent(SettingsStore.getValue("theme"));
if (id) { if (id) {
url += '&integ_id=' + encodeURIComponent(id); url += '&integ_id=' + encodeURIComponent(id);
} }

View file

@ -366,6 +366,22 @@ function getWidgets(event, roomId) {
sendResponse(event, widgetStateEvents); sendResponse(event, widgetStateEvents);
} }
function getRoomEncState(event, roomId) {
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
return;
}
const room = client.getRoom(roomId);
if (!room) {
sendError(event, _t('This room is not recognised.'));
return;
}
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);
sendResponse(event, roomIsEncrypted);
}
function setPlumbingState(event, roomId, status) { function setPlumbingState(event, roomId, status) {
if (typeof status !== 'string') { if (typeof status !== 'string') {
throw new Error('Plumbing state status should be a string'); throw new Error('Plumbing state status should be a string');
@ -541,8 +557,16 @@ const onMessage = function(event) {
// //
// All strings start with the empty string, so for sanity return if the length // All strings start with the empty string, so for sanity return if the length
// of the event origin is 0. // of the event origin is 0.
//
// TODO -- Scalar postMessage API should be namespaced with event.data.api field
// Fix following "if" statement to respond only to specific API messages.
const url = SdkConfig.get().integrations_ui_url; const url = SdkConfig.get().integrations_ui_url;
if (event.origin.length === 0 || !url.startsWith(event.origin) || !event.data.action) { if (
event.origin.length === 0 ||
!url.startsWith(event.origin) ||
!event.data.action ||
event.data.api // Ignore messages with specific API set
) {
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
} }
@ -593,6 +617,9 @@ const onMessage = function(event) {
} else if (event.data.action === "get_widgets") { } else if (event.data.action === "get_widgets") {
getWidgets(event, roomId); getWidgets(event, roomId);
return; return;
} else if (event.data.action === "get_room_enc_state") {
getRoomEncState(event, roomId);
return;
} else if (event.data.action === "can_send_event") { } else if (event.data.action === "can_send_event") {
canSendEvent(event, roomId); canSendEvent(event, roomId);
return; return;

View file

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

View file

@ -20,6 +20,7 @@ import Tinter from "./Tinter";
import sdk from './index'; import sdk from './index';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import Modal from './Modal'; import Modal from './Modal';
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
class Command { class Command {
@ -97,9 +98,7 @@ const commands = {
colorScheme.secondary_color = matches[4]; colorScheme.secondary_color = matches[4];
} }
return success( return success(
MatrixClientPeg.get().setRoomAccountData( SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
roomId, "org.matrix.room.color_scheme", colorScheme,
),
); );
} }
} }

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015 OpenMarket Ltd Copyright 2015 OpenMarket Ltd
Copyright 2017 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,17 +15,53 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// FIXME: these vars should be bundled up and attached to
// module.exports otherwise this will break when included by both
// react-sdk and apps layered on top.
const DEBUG = 0; const DEBUG = 0;
// The colour keys to be replaced as referred to in CSS // utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue]
const keyRgb = [ function colorToRgb(color) {
if (!color) {
return [0, 0, 0];
}
if (color[0] === '#') {
color = color.slice(1);
if (color.length === 3) {
color = color[0] + color[0] +
color[1] + color[1] +
color[2] + color[2];
}
const val = parseInt(color, 16);
const r = (val >> 16) & 255;
const g = (val >> 8) & 255;
const b = val & 255;
return [r, g, b];
} else {
const match = color.match(/rgb\((.*?),(.*?),(.*?)\)/);
if (match) {
return [
parseInt(match[1]),
parseInt(match[2]),
parseInt(match[3]),
];
}
}
return [0, 0, 0];
}
// utility to turn [red,green,blue] into #rrggbb
function rgbToColor(rgb) {
const val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
return '#' + (0x1000000 + val).toString(16).slice(1);
}
class Tinter {
constructor() {
// The default colour keys to be replaced as referred to in CSS
// (should be overridden by .mx_theme_accentColor and .mx_theme_secondaryAccentColor)
this.keyRgb = [
"rgb(118, 207, 166)", // Vector Green "rgb(118, 207, 166)", // Vector Green
"rgb(234, 245, 240)", // Vector Light Green "rgb(234, 245, 240)", // Vector Light Green
"rgb(211, 239, 225)", // BottomLeftMenu overlay (20% Vector Green) "rgb(211, 239, 225)", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
]; ];
// Some algebra workings for calculating the tint % of Vector Green & Light Green // Some algebra workings for calculating the tint % of Vector Green & Light Green
@ -35,32 +72,45 @@ const keyRgb = [
// x = (255 - 234) / (255 - 118) = 0.16 // x = (255 - 234) / (255 - 118) = 0.16
// The colour keys to be replaced as referred to in SVGs // The colour keys to be replaced as referred to in SVGs
const keyHex = [ this.keyHex = [
"#76CFA6", // Vector Green "#76CFA6", // Vector Green
"#EAF5F0", // Vector Light Green "#EAF5F0", // Vector Light Green
"#D3EFE1", // BottomLeftMenu overlay (20% Vector Green overlaid on Vector Light Green) "#D3EFE1", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
"#FFFFFF", // white highlights of the SVGs (for switching to dark theme) "#FFFFFF", // white highlights of the SVGs (for switching to dark theme)
"#000000", // black lowlights of the SVGs (for switching to dark theme)
]; ];
// cache of our replacement colours // track the replacement colours actually being used
// defaults to our keys. // defaults to our keys.
const colors = [ this.colors = [
keyHex[0], this.keyHex[0],
keyHex[1], this.keyHex[1],
keyHex[2], this.keyHex[2],
keyHex[3], this.keyHex[3],
this.keyHex[4],
]; ];
const cssFixups = [ // track the most current tint request inputs (which may differ from the
// { // end result stored in this.colors
this.currentTint = [
undefined,
undefined,
undefined,
undefined,
undefined,
];
this.cssFixups = [
// { theme: {
// style: a style object that should be fixed up taken from a stylesheet // style: a style object that should be fixed up taken from a stylesheet
// attr: name of the attribute to be clobbered, e.g. 'color' // attr: name of the attribute to be clobbered, e.g. 'color'
// index: ordinal of primary, secondary or tertiary // index: ordinal of primary, secondary or tertiary
// },
// } // }
]; ];
// CSS attributes to be fixed up // CSS attributes to be fixed up
const cssAttrs = [ this.cssAttrs = [
"color", "color",
"backgroundColor", "backgroundColor",
"borderColor", "borderColor",
@ -69,15 +119,184 @@ const cssAttrs = [
"borderLeftColor", "borderLeftColor",
]; ];
const svgAttrs = [ this.svgAttrs = [
"fill", "fill",
"stroke", "stroke",
]; ];
let cached = false; // List of functions to call when the tint changes.
this.tintables = [];
// the currently loaded theme (if any)
this.theme = undefined;
// whether to force a tint (e.g. after changing theme)
this.forceTint = false;
}
/**
* Register a callback to fire when the tint changes.
* This is used to rewrite the tintable SVGs with the new tint.
*
* It's not possible to unregister a tintable callback. So this can only be
* used to register a static callback. If a set of tintables will change
* over time then the best bet is to register a single callback for the
* entire set.
*
* @param {Function} tintable Function to call when the tint changes.
*/
registerTintable(tintable) {
this.tintables.push(tintable);
}
getKeyRgb() {
return this.keyRgb;
}
tint(primaryColor, secondaryColor, tertiaryColor) {
this.currentTint[0] = primaryColor;
this.currentTint[1] = secondaryColor;
this.currentTint[2] = tertiaryColor;
this.calcCssFixups();
if (DEBUG) {
console.log("Tinter.tint(" + primaryColor + ", " +
secondaryColor + ", " +
tertiaryColor + ")");
}
if (!primaryColor) {
primaryColor = this.keyRgb[0];
secondaryColor = this.keyRgb[1];
tertiaryColor = this.keyRgb[2];
}
if (!secondaryColor) {
const x = 0.16; // average weighting factor calculated from vector green & light green
const rgb = colorToRgb(primaryColor);
rgb[0] = x * rgb[0] + (1 - x) * 255;
rgb[1] = x * rgb[1] + (1 - x) * 255;
rgb[2] = x * rgb[2] + (1 - x) * 255;
secondaryColor = rgbToColor(rgb);
}
if (!tertiaryColor) {
const x = 0.19;
const rgb1 = colorToRgb(primaryColor);
const rgb2 = colorToRgb(secondaryColor);
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1];
rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2];
tertiaryColor = rgbToColor(rgb1);
}
if (this.forceTint == false &&
this.colors[0] === primaryColor &&
this.colors[1] === secondaryColor &&
this.colors[2] === tertiaryColor) {
return;
}
this.forceTint = false;
this.colors[0] = primaryColor;
this.colors[1] = secondaryColor;
this.colors[2] = tertiaryColor;
if (DEBUG) {
console.log("Tinter.tint final: (" + primaryColor + ", " +
secondaryColor + ", " +
tertiaryColor + ")");
}
// go through manually fixing up the stylesheets.
this.applyCssFixups();
// tell all the SVGs to go fix themselves up
// we don't do this as a dispatch otherwise it will visually lag
this.tintables.forEach(function(tintable) {
tintable();
});
}
tintSvgWhite(whiteColor) {
this.currentTint[3] = whiteColor;
if (!whiteColor) {
whiteColor = this.colors[3];
}
if (this.colors[3] === whiteColor) {
return;
}
this.colors[3] = whiteColor;
this.tintables.forEach(function(tintable) {
tintable();
});
}
tintSvgBlack(blackColor) {
this.currentTint[4] = blackColor;
if (!blackColor) {
blackColor = this.colors[4];
}
if (this.colors[4] === blackColor) {
return;
}
this.colors[4] = blackColor;
this.tintables.forEach(function(tintable) {
tintable();
});
}
setTheme(theme) {
console.trace("setTheme " + theme);
this.theme = theme;
// update keyRgb from the current theme CSS itself, if it defines it
if (document.getElementById('mx_theme_accentColor')) {
this.keyRgb[0] = window.getComputedStyle(
document.getElementById('mx_theme_accentColor')).color;
}
if (document.getElementById('mx_theme_secondaryAccentColor')) {
this.keyRgb[1] = window.getComputedStyle(
document.getElementById('mx_theme_secondaryAccentColor')).color;
}
if (document.getElementById('mx_theme_tertiaryAccentColor')) {
this.keyRgb[2] = window.getComputedStyle(
document.getElementById('mx_theme_tertiaryAccentColor')).color;
}
this.calcCssFixups();
this.forceTint = true;
this.tint(this.currentTint[0], this.currentTint[1], this.currentTint[2]);
if (theme === 'dark') {
// abuse the tinter to change all the SVG's #fff to #2d2d2d
// XXX: obviously this shouldn't be hardcoded here.
this.tintSvgWhite('#2d2d2d');
this.tintSvgBlack('#dddddd');
} else {
this.tintSvgWhite('#ffffff');
this.tintSvgBlack('#000000');
}
}
calcCssFixups() {
// cache our fixups
if (this.cssFixups[this.theme]) return;
if (DEBUG) {
console.debug("calcCssFixups start for " + this.theme + " (checking " +
document.styleSheets.length +
" stylesheets)");
}
this.cssFixups[this.theme] = [];
function calcCssFixups() {
if (DEBUG) console.log("calcSvgFixups start");
for (let i = 0; i < document.styleSheets.length; i++) { for (let i = 0; i < document.styleSheets.length; i++) {
const ss = document.styleSheets[i]; const ss = document.styleSheets[i];
if (!ss) continue; // well done safari >:( if (!ss) continue; // well done safari >:(
@ -100,18 +319,29 @@ function calcCssFixups() {
// Iterating through the CSS looking for matches to hack on feels // Iterating through the CSS looking for matches to hack on feels
// pretty horrible anyway. And what if the application skin doesn't use // pretty horrible anyway. And what if the application skin doesn't use
// Vector Green as its primary color? // Vector Green as its primary color?
// --richvdh
if (ss.href && !ss.href.match(/\/bundle.*\.css$/)) continue; // Yes, tinting assumes that you are using the Riot skin for now.
// The right solution will be to move the CSS over to react-sdk.
// And yes, the default assets for the base skin might as well use
// Vector Green as any other colour.
// --matthew
if (ss.href && !ss.href.match(new RegExp('/theme-' + this.theme + '.css$'))) continue;
if (ss.disabled) continue;
if (!ss.cssRules) continue; if (!ss.cssRules) continue;
if (DEBUG) console.debug("calcCssFixups checking " + ss.cssRules.length + " rules for " + ss.href);
for (let j = 0; j < ss.cssRules.length; j++) { for (let j = 0; j < ss.cssRules.length; j++) {
const rule = ss.cssRules[j]; const rule = ss.cssRules[j];
if (!rule.style) continue; if (!rule.style) continue;
for (let k = 0; k < cssAttrs.length; k++) { if (rule.selectorText && rule.selectorText.match(/#mx_theme/)) continue;
const attr = cssAttrs[k]; for (let k = 0; k < this.cssAttrs.length; k++) {
for (let l = 0; l < keyRgb.length; l++) { const attr = this.cssAttrs[k];
if (rule.style[attr] === keyRgb[l]) { for (let l = 0; l < this.keyRgb.length; l++) {
cssFixups.push({ if (rule.style[attr] === this.keyRgb[l]) {
this.cssFixups[this.theme].push({
style: rule.style, style: rule.style,
attr: attr, attr: attr,
index: l, index: l,
@ -121,125 +351,37 @@ function calcCssFixups() {
} }
} }
} }
if (DEBUG) console.log("calcSvgFixups end"); if (DEBUG) {
console.log("calcCssFixups end (" +
this.cssFixups[this.theme].length +
" fixups)");
}
} }
function applyCssFixups() { applyCssFixups() {
if (DEBUG) console.log("applyCssFixups start"); if (DEBUG) {
for (let i = 0; i < cssFixups.length; i++) { console.log("applyCssFixups start (" +
const cssFixup = cssFixups[i]; this.cssFixups[this.theme].length +
cssFixup.style[cssFixup.attr] = colors[cssFixup.index]; " fixups)");
}
for (let i = 0; i < this.cssFixups[this.theme].length; i++) {
const cssFixup = this.cssFixups[this.theme][i];
try {
cssFixup.style[cssFixup.attr] = this.colors[cssFixup.index];
} catch (e) {
// Firefox Quantum explodes if you manually edit the CSS in the
// inspector and then try to do a tint, as apparently all the
// fixups are then stale.
console.error("Failed to apply cssFixup in Tinter! ", e.name);
}
} }
if (DEBUG) console.log("applyCssFixups end"); if (DEBUG) console.log("applyCssFixups end");
} }
function hexToRgb(color) {
if (color[0] === '#') color = color.slice(1);
if (color.length === 3) {
color = color[0] + color[0] +
color[1] + color[1] +
color[2] + color[2];
}
const val = parseInt(color, 16);
const r = (val >> 16) & 255;
const g = (val >> 8) & 255;
const b = val & 255;
return [r, g, b];
}
function rgbToHex(rgb) {
const val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
return '#' + (0x1000000 + val).toString(16).slice(1);
}
// List of functions to call when the tint changes.
const tintables = [];
module.exports = {
/**
* Register a callback to fire when the tint changes.
* This is used to rewrite the tintable SVGs with the new tint.
*
* It's not possible to unregister a tintable callback. So this can only be
* used to register a static callback. If a set of tintables will change
* over time then the best bet is to register a single callback for the
* entire set.
*
* @param {Function} tintable Function to call when the tint changes.
*/
registerTintable: function(tintable) {
tintables.push(tintable);
},
tint: function(primaryColor, secondaryColor, tertiaryColor) {
if (!cached) {
calcCssFixups();
cached = true;
}
if (!primaryColor) {
primaryColor = "#76CFA6"; // Vector green
secondaryColor = "#EAF5F0"; // Vector light green
}
if (!secondaryColor) {
const x = 0.16; // average weighting factor calculated from vector green & light green
const rgb = hexToRgb(primaryColor);
rgb[0] = x * rgb[0] + (1 - x) * 255;
rgb[1] = x * rgb[1] + (1 - x) * 255;
rgb[2] = x * rgb[2] + (1 - x) * 255;
secondaryColor = rgbToHex(rgb);
}
if (!tertiaryColor) {
const x = 0.19;
const rgb1 = hexToRgb(primaryColor);
const rgb2 = hexToRgb(secondaryColor);
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1];
rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2];
tertiaryColor = rgbToHex(rgb1);
}
if (colors[0] === primaryColor &&
colors[1] === secondaryColor &&
colors[2] === tertiaryColor) {
return;
}
colors[0] = primaryColor;
colors[1] = secondaryColor;
colors[2] = tertiaryColor;
if (DEBUG) console.log("Tinter.tint");
// go through manually fixing up the stylesheets.
applyCssFixups();
// tell all the SVGs to go fix themselves up
// we don't do this as a dispatch otherwise it will visually lag
tintables.forEach(function(tintable) {
tintable();
});
},
tintSvgWhite: function(whiteColor) {
if (!whiteColor) {
whiteColor = colors[3];
}
if (colors[3] === whiteColor) {
return;
}
colors[3] = whiteColor;
tintables.forEach(function(tintable) {
tintable();
});
},
// XXX: we could just move this all into TintableSvg, but as it's so similar // XXX: we could just move this all into TintableSvg, but as it's so similar
// to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg) // to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
// keeping it here for now. // keeping it here for now.
calcSvgFixups: function(svgs) { calcSvgFixups(svgs) {
// go through manually fixing up SVG colours. // go through manually fixing up SVG colours.
// we could do this by stylesheets, but keeping the stylesheets // we could do this by stylesheets, but keeping the stylesheets
// updated would be a PITA, so just brute-force search for the // updated would be a PITA, so just brute-force search for the
@ -248,7 +390,7 @@ module.exports = {
if (DEBUG) console.log("calcSvgFixups start for " + svgs); if (DEBUG) console.log("calcSvgFixups start for " + svgs);
const fixups = []; const fixups = [];
for (let i = 0; i < svgs.length; i++) { for (let i = 0; i < svgs.length; i++) {
var svgDoc; let svgDoc;
try { try {
svgDoc = svgs[i].contentDocument; svgDoc = svgs[i].contentDocument;
} catch (e) { } catch (e) {
@ -259,16 +401,17 @@ module.exports = {
if (e.stack) { if (e.stack) {
msg += ' | stack: ' + e.stack; msg += ' | stack: ' + e.stack;
} }
console.error(e); console.error(msg);
} }
if (!svgDoc) continue; if (!svgDoc) continue;
const tags = svgDoc.getElementsByTagName("*"); const tags = svgDoc.getElementsByTagName("*");
for (let j = 0; j < tags.length; j++) { for (let j = 0; j < tags.length; j++) {
const tag = tags[j]; const tag = tags[j];
for (let k = 0; k < svgAttrs.length; k++) { for (let k = 0; k < this.svgAttrs.length; k++) {
const attr = svgAttrs[k]; const attr = this.svgAttrs[k];
for (let l = 0; l < keyHex.length; l++) { for (let l = 0; l < this.keyHex.length; l++) {
if (tag.getAttribute(attr) && tag.getAttribute(attr).toUpperCase() === keyHex[l]) { if (tag.getAttribute(attr) &&
tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) {
fixups.push({ fixups.push({
node: tag, node: tag,
attr: attr, attr: attr,
@ -282,14 +425,19 @@ module.exports = {
if (DEBUG) console.log("calcSvgFixups end"); if (DEBUG) console.log("calcSvgFixups end");
return fixups; return fixups;
}, }
applySvgFixups: function(fixups) { applySvgFixups(fixups) {
if (DEBUG) console.log("applySvgFixups start for " + fixups); if (DEBUG) console.log("applySvgFixups start for " + fixups);
for (let i = 0; i < fixups.length; i++) { for (let i = 0; i < fixups.length; i++) {
const svgFixup = fixups[i]; const svgFixup = fixups[i];
svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]); svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]);
} }
if (DEBUG) console.log("applySvgFixups end"); if (DEBUG) console.log("applySvgFixups end");
}, }
}; }
if (global.singletonTinter === undefined) {
global.singletonTinter = new Tinter();
}
export default global.singletonTinter;

View file

@ -1,51 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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 dis from './dispatcher';
import sdk from './index';
import Modal from './Modal';
let isDialogOpen = false;
const onAction = function(payload) {
if (payload.action === 'unknown_device_error' && !isDialogOpen) {
const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog');
isDialogOpen = true;
Modal.createTrackedDialog('Unknown Device Error', '', UnknownDeviceDialog, {
devices: payload.err.devices,
room: payload.room,
onFinished: (r) => {
isDialogOpen = false;
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
console.log('UnknownDeviceDialog closed with '+r);
},
}, 'mx_Dialog_unknownDevice');
}
};
let ref = null;
export function startListening() {
ref = dis.register(onAction);
}
export function stopListening() {
if (ref) {
dis.unregister(ref);
ref = null;
}
}

View file

@ -15,7 +15,6 @@ limitations under the License.
*/ */
const MatrixClientPeg = require('./MatrixClientPeg'); const MatrixClientPeg = require('./MatrixClientPeg');
import UserSettingsStore from './UserSettingsStore';
import shouldHideEvent from './shouldHideEvent'; import shouldHideEvent from './shouldHideEvent';
const sdk = require('./index'); const sdk = require('./index');
@ -64,7 +63,6 @@ module.exports = {
// we have and the read receipt. We could fetch more history to try & find out, // we have and the read receipt. We could fetch more history to try & find out,
// but currently we just guess. // but currently we just guess.
const syncedSettings = UserSettingsStore.getSyncedSettings();
// Loop through messages, starting with the most recent... // Loop through messages, starting with the most recent...
for (let i = room.timeline.length - 1; i >= 0; --i) { for (let i = room.timeline.length - 1; i >= 0; --i) {
const ev = room.timeline[i]; const ev = room.timeline[i];
@ -74,7 +72,7 @@ module.exports = {
// that counts and we can stop looking because the user's read // that counts and we can stop looking because the user's read
// this and everything before. // this and everything before.
return false; return false;
} else if (!shouldHideEvent(ev, syncedSettings) && this.eventTriggersUnreadCount(ev)) { } else if (!shouldHideEvent(ev) && this.eventTriggersUnreadCount(ev)) {
// We've found a message that counts before we hit // We've found a message that counts before we hit
// the read marker, so this room is definitely unread. // the read marker, so this room is definitely unread.
return true; return true;

View file

@ -17,54 +17,11 @@ limitations under the License.
import Promise from 'bluebird'; import Promise from 'bluebird';
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import Notifier from './Notifier';
import { _t, _td } from './languageHandler';
import SdkConfig from './SdkConfig';
/* /*
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage. * TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
*/ */
const FEATURES = [
{
id: 'feature_pinning',
name: _td("Message Pinning"),
},
];
export default { export default {
getLabsFeatures() {
const featuresConfig = SdkConfig.get()['features'] || {};
// The old flag: honoured for backwards compatibility
const enableLabs = SdkConfig.get()['enableLabs'];
let labsFeatures;
if (enableLabs) {
labsFeatures = FEATURES;
} else {
labsFeatures = FEATURES.filter((f) => {
const sdkConfigValue = featuresConfig[f.id];
if (sdkConfigValue === 'labs') {
return true;
}
});
}
return labsFeatures.map((f) => {
return f.id;
});
},
translatedNameForFeature(featureId) {
const feature = FEATURES.filter((f) => {
return f.id === featureId;
})[0];
if (feature === undefined) return null;
return _t(feature.name);
},
loadProfileInfo: function() { loadProfileInfo: function() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
return cli.getProfileInfo(cli.credentials.userId); return cli.getProfileInfo(cli.credentials.userId);
@ -87,36 +44,6 @@ export default {
// TODO // TODO
}, },
getEnableNotifications: function() {
return Notifier.isEnabled();
},
setEnableNotifications: function(enable) {
if (!Notifier.supportsDesktopNotifications()) {
return;
}
Notifier.setEnabled(enable);
},
getEnableNotificationBody: function() {
return Notifier.isBodyEnabled();
},
setEnableNotificationBody: function(enable) {
if (!Notifier.supportsDesktopNotifications()) {
return;
}
Notifier.setBodyEnabled(enable);
},
getEnableAudioNotifications: function() {
return Notifier.isAudioEnabled();
},
setEnableAudioNotifications: function(enable) {
Notifier.setAudioEnabled(enable);
},
changePassword: function(oldPassword, newPassword) { changePassword: function(oldPassword, newPassword) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -163,83 +90,4 @@ export default {
append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address
}); });
}, },
getUrlPreviewsDisabled: function() {
const event = MatrixClientPeg.get().getAccountData('org.matrix.preview_urls');
return (event && event.getContent().disable);
},
setUrlPreviewsDisabled: function(disabled) {
// FIXME: handle errors
return MatrixClientPeg.get().setAccountData('org.matrix.preview_urls', {
disable: disabled,
});
},
getSyncedSettings: function() {
const event = MatrixClientPeg.get().getAccountData('im.vector.web.settings');
return event ? event.getContent() : {};
},
getSyncedSetting: function(type, defaultValue = null) {
const settings = this.getSyncedSettings();
return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
},
setSyncedSetting: function(type, value) {
const settings = this.getSyncedSettings();
settings[type] = value;
// FIXME: handle errors
return MatrixClientPeg.get().setAccountData('im.vector.web.settings', settings);
},
getLocalSettings: function() {
const localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
return JSON.parse(localSettingsString);
},
getLocalSetting: function(type, defaultValue = null) {
const settings = this.getLocalSettings();
return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
},
setLocalSetting: function(type, value) {
const settings = this.getLocalSettings();
settings[type] = value;
// FIXME: handle errors
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
},
isFeatureEnabled: function(featureId: string): boolean {
const featuresConfig = SdkConfig.get()['features'];
// The old flag: honoured for backwards compatibility
const enableLabs = SdkConfig.get()['enableLabs'];
let sdkConfigValue = enableLabs ? 'labs' : 'disable';
if (featuresConfig && featuresConfig[featureId] !== undefined) {
sdkConfigValue = featuresConfig[featureId];
}
if (sdkConfigValue === 'enable') {
return true;
} else if (sdkConfigValue === 'disable') {
return false;
} else if (sdkConfigValue === 'labs') {
if (!MatrixClientPeg.get().isGuest()) {
// Make it explicit that guests get the defaults (although they shouldn't
// have been able to ever toggle the flags anyway)
const userValue = localStorage.getItem(`mx_labs_feature_${featureId}`);
return userValue === 'true';
}
return false;
} else {
console.warn(`Unknown features config for ${featureId}: ${sdkConfigValue}`);
return false;
}
},
setFeatureEnabled: function(featureId: string, enabled: boolean) {
localStorage.setItem(`mx_labs_feature_${featureId}`, enabled);
},
}; };

326
src/WidgetMessaging.js Normal file
View file

@ -0,0 +1,326 @@
/*
Copyright 2017 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.
*/
/*
Listens for incoming postMessage requests from embedded widgets. The following API is exposed:
{
api: "widget",
action: "content_loaded",
widgetId: $WIDGET_ID,
data: {}
// additional request fields
}
The complete request object is returned to the caller with an additional "response" key like so:
{
api: "widget",
action: "content_loaded",
widgetId: $WIDGET_ID,
data: {},
// additional request fields
response: { ... }
}
The "api" field is required to use this API, and must be set to "widget" in all requests.
The "action" determines the format of the request and response. All actions can return an error response.
Additional data can be sent as additional, abritrary fields. However, typically the data object should be used.
A success response is an object with zero or more keys.
An error response is a "response" object which consists of a sole "error" key to indicate an error.
They look like:
{
error: {
message: "Unable to invite user into room.",
_error: <Original Error Object>
}
}
The "message" key should be a human-friendly string.
ACTIONS
=======
** All actions must include an "api" field with valie "widget".**
All actions can return an error response instead of the response outlined below.
content_loaded
--------------
Indicates that widget contet has fully loaded
Request:
- widgetId is the unique ID of the widget instance in riot / matrix state.
- No additional fields.
Response:
{
success: true
}
Example:
{
api: "widget",
action: "content_loaded",
widgetId: $WIDGET_ID
}
api_version
-----------
Get the current version of the widget postMessage API
Request:
- No additional fields.
Response:
{
api_version: "0.0.1"
}
Example:
{
api: "widget",
action: "api_version",
}
supported_api_versions
----------------------
Get versions of the widget postMessage API that are currently supported
Request:
- No additional fields.
Response:
{
api: "widget"
supported_versions: ["0.0.1"]
}
Example:
{
api: "widget",
action: "supported_api_versions",
}
*/
import URL from 'url';
const WIDGET_API_VERSION = '0.0.1'; // Current API version
const SUPPORTED_WIDGET_API_VERSIONS = [
'0.0.1',
];
import dis from './dispatcher';
if (!global.mxWidgetMessagingListenerCount) {
global.mxWidgetMessagingListenerCount = 0;
}
if (!global.mxWidgetMessagingMessageEndpoints) {
global.mxWidgetMessagingMessageEndpoints = [];
}
/**
* Register widget message event listeners
*/
function startListening() {
if (global.mxWidgetMessagingListenerCount === 0) {
window.addEventListener("message", onMessage, false);
}
global.mxWidgetMessagingListenerCount += 1;
}
/**
* De-register widget message event listeners
*/
function stopListening() {
global.mxWidgetMessagingListenerCount -= 1;
if (global.mxWidgetMessagingListenerCount === 0) {
window.removeEventListener("message", onMessage);
}
if (global.mxWidgetMessagingListenerCount < 0) {
// Make an error so we get a stack trace
const e = new Error(
"WidgetMessaging: mismatched startListening / stopListening detected." +
" Negative count",
);
console.error(e);
}
}
/**
* Register a widget endpoint for trusted postMessage communication
* @param {string} widgetId Unique widget identifier
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
*/
function addEndpoint(widgetId, endpointUrl) {
const u = URL.parse(endpointUrl);
if (!u || !u.protocol || !u.host) {
console.warn("Invalid origin:", endpointUrl);
return;
}
const origin = u.protocol + '//' + u.host;
const endpoint = new WidgetMessageEndpoint(widgetId, origin);
if (global.mxWidgetMessagingMessageEndpoints) {
if (global.mxWidgetMessagingMessageEndpoints.some(function(ep) {
return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
})) {
// Message endpoint already registered
console.warn("Endpoint already registered");
return;
}
global.mxWidgetMessagingMessageEndpoints.push(endpoint);
}
}
/**
* De-register a widget endpoint from trusted communication sources
* @param {string} widgetId Unique widget identifier
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
* @return {boolean} True if endpoint was successfully removed
*/
function removeEndpoint(widgetId, endpointUrl) {
const u = URL.parse(endpointUrl);
if (!u || !u.protocol || !u.host) {
console.warn("Invalid origin");
return;
}
const origin = u.protocol + '//' + u.host;
if (global.mxWidgetMessagingMessageEndpoints && global.mxWidgetMessagingMessageEndpoints.length > 0) {
const length = global.mxWidgetMessagingMessageEndpoints.length;
global.mxWidgetMessagingMessageEndpoints = global.mxWidgetMessagingMessageEndpoints.filter(function(endpoint) {
return (endpoint.widgetId != widgetId || endpoint.endpointUrl != origin);
});
return (length > global.mxWidgetMessagingMessageEndpoints.length);
}
return false;
}
/**
* Handle widget postMessage events
* @param {Event} event Event to handle
* @return {undefined}
*/
function onMessage(event) {
if (!event.origin) { // Handle chrome
event.origin = event.originalEvent.origin;
}
// Event origin is empty string if undefined
if (
event.origin.length === 0 ||
!trustedEndpoint(event.origin) ||
event.data.api !== "widget" ||
!event.data.widgetId
) {
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
}
const action = event.data.action;
const widgetId = event.data.widgetId;
if (action === 'content_loaded') {
dis.dispatch({
action: 'widget_content_loaded',
widgetId: widgetId,
});
sendResponse(event, {success: true});
} else if (action === 'supported_api_versions') {
sendResponse(event, {
api: "widget",
supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
});
} else if (action === 'api_version') {
sendResponse(event, {
api: "widget",
version: WIDGET_API_VERSION,
});
} else {
console.warn("Widget postMessage event unhandled");
sendError(event, {message: "The postMessage was unhandled"});
}
}
/**
* Check if message origin is registered as trusted
* @param {string} origin PostMessage origin to check
* @return {boolean} True if trusted
*/
function trustedEndpoint(origin) {
if (!origin) {
return false;
}
return global.mxWidgetMessagingMessageEndpoints.some((endpoint) => {
return endpoint.endpointUrl === origin;
});
}
/**
* Send a postmessage response to a postMessage request
* @param {Event} event The original postMessage request event
* @param {Object} res Response data
*/
function sendResponse(event, res) {
const data = JSON.parse(JSON.stringify(event.data));
data.response = res;
event.source.postMessage(data, event.origin);
}
/**
* Send an error response to a postMessage request
* @param {Event} event The original postMessage request event
* @param {string} msg Error message
* @param {Error} nestedError Nested error event (optional)
*/
function sendError(event, msg, nestedError) {
console.error("Action:" + event.data.action + " failed with message: " + msg);
const data = JSON.parse(JSON.stringify(event.data));
data.response = {
error: {
message: msg,
},
};
if (nestedError) {
data.response.error._error = nestedError;
}
event.source.postMessage(data, event.origin);
}
/**
* Represents mapping of widget instance to URLs for trusted postMessage communication.
*/
class WidgetMessageEndpoint {
/**
* Mapping of widget instance to URL for trusted postMessage communication.
* @param {string} widgetId Unique widget identifier
* @param {string} endpointUrl Widget wurl origin.
*/
constructor(widgetId, endpointUrl) {
if (!widgetId) {
throw new Error("No widgetId specified in widgetMessageEndpoint constructor");
}
if (!endpointUrl) {
throw new Error("No endpoint specified in widgetMessageEndpoint constructor");
}
this.widgetId = widgetId;
this.endpointUrl = endpointUrl;
}
}
export default {
startListening: startListening,
stopListening: stopListening,
addEndpoint: addEndpoint,
removeEndpoint: removeEndpoint,
};

View file

@ -0,0 +1,34 @@
/*
Copyright 2017 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 { asyncAction } from './actionCreators';
const GroupActions = {};
/**
* Creates an action thunk that will do an asynchronous request to fetch
* the groups to which a user is joined.
*
* @param {MatrixClient} matrixClient the matrix client to query.
* @returns {function} an action thunk that will dispatch actions
* indicating the status of the request.
* @see asyncAction
*/
GroupActions.fetchJoinedGroups = function(matrixClient) {
return asyncAction('GroupActions.fetchJoinedGroups', () => matrixClient.getJoinedGroups());
};
export default GroupActions;

View file

@ -0,0 +1,108 @@
/*
Copyright 2017 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 dis from '../dispatcher';
// TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events
// become dispatches in the same place.
/**
* Create a MatrixActions.sync action that represents a MatrixClient `sync` event,
* each parameter mapping to a key-value in the action.
*
* @param {MatrixClient} matrixClient the matrix client
* @param {string} state the current sync state.
* @param {string} prevState the previous sync state.
* @returns {Object} an action of type MatrixActions.sync.
*/
function createSyncAction(matrixClient, state, prevState) {
return {
action: 'MatrixActions.sync',
state,
prevState,
matrixClient,
};
}
/**
* @typedef AccountDataAction
* @type {Object}
* @property {string} action 'MatrixActions.accountData'.
* @property {MatrixEvent} event the MatrixEvent that triggered the dispatch.
* @property {string} event_type the type of the MatrixEvent, e.g. "m.direct".
* @property {Object} event_content the content of the MatrixEvent.
*/
/**
* Create a MatrixActions.accountData action that represents a MatrixClient `accountData`
* matrix event.
*
* @param {MatrixClient} matrixClient the matrix client.
* @param {MatrixEvent} accountDataEvent the account data event.
* @returns {AccountDataAction} an action of type MatrixActions.accountData.
*/
function createAccountDataAction(matrixClient, accountDataEvent) {
return {
action: 'MatrixActions.accountData',
event: accountDataEvent,
event_type: accountDataEvent.getType(),
event_content: accountDataEvent.getContent(),
};
}
/**
* This object is responsible for dispatching actions when certain events are emitted by
* the given MatrixClient.
*/
export default {
// A list of callbacks to call to unregister all listeners added
_matrixClientListenersStop: [],
/**
* Start listening to certain events from the MatrixClient and dispatch actions when
* they are emitted.
* @param {MatrixClient} matrixClient the MatrixClient to listen to events from
*/
start(matrixClient) {
this._addMatrixClientListener(matrixClient, 'sync', createSyncAction);
this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
},
/**
* Start listening to events of type eventName on matrixClient and when they are emitted,
* dispatch an action created by the actionCreator function.
* @param {MatrixClient} matrixClient a MatrixClient to register a listener with.
* @param {string} eventName the event to listen to on MatrixClient.
* @param {function} actionCreator a function that should return an action to dispatch
* when given the MatrixClient as an argument as well as
* arguments emitted in the MatrixClient event.
*/
_addMatrixClientListener(matrixClient, eventName, actionCreator) {
const listener = (...args) => {
dis.dispatch(actionCreator(matrixClient, ...args));
};
matrixClient.on(eventName, listener);
this._matrixClientListenersStop.push(() => {
matrixClient.removeListener(eventName, listener);
});
},
/**
* Stop listening to events.
*/
stop() {
this._matrixClientListenersStop.forEach((stopListener) => stopListener());
},
};

View file

@ -0,0 +1,47 @@
/*
Copyright 2017 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 Analytics from '../Analytics';
import { asyncAction } from './actionCreators';
import TagOrderStore from '../stores/TagOrderStore';
const TagOrderActions = {};
/**
* Creates an action thunk that will do an asynchronous request to
* commit TagOrderStore.getOrderedTags() to account data and dispatch
* actions to indicate the status of the request.
*
* @param {MatrixClient} matrixClient the matrix client to set the
* account data on.
* @returns {function} an action thunk that will dispatch actions
* indicating the status of the request.
* @see asyncAction
*/
TagOrderActions.commitTagOrdering = function(matrixClient) {
return asyncAction('TagOrderActions.commitTagOrdering', () => {
// Only commit tags if the state is ready, i.e. not null
const tags = TagOrderStore.getOrderedTags();
if (!tags) {
return;
}
Analytics.trackEvent('TagOrderActions', 'commitTagOrdering');
return matrixClient.setAccountData('im.vector.web.tag_ordering', {tags});
});
};
export default TagOrderActions;

View file

@ -0,0 +1,41 @@
/*
Copyright 2017 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.
*/
/**
* Create an action thunk that will dispatch actions indicating the current
* status of the Promise returned by fn.
*
* @param {string} id the id to give the dispatched actions. This is given a
* suffix determining whether it is pending, successful or
* a failure.
* @param {function} fn a function that returns a Promise.
* @returns {function} an action thunk - a function that uses its single
* argument as a dispatch function to dispatch the
* following actions:
* `${id}.pending` and either
* `${id}.success` or
* `${id}.failure`.
*/
export function asyncAction(id, fn) {
return (dispatch) => {
dispatch({action: id + '.pending'});
fn().then((result) => {
dispatch({action: id + '.success', result});
}).catch((err) => {
dispatch({action: id + '.failure', err});
});
};
}

View file

@ -26,7 +26,7 @@ import {PillCompletion} from './Components';
import type {SelectionRange, Completion} from './Autocompleter'; import type {SelectionRange, Completion} from './Autocompleter';
import _uniq from 'lodash/uniq'; import _uniq from 'lodash/uniq';
import _sortBy from 'lodash/sortBy'; import _sortBy from 'lodash/sortBy';
import UserSettingsStore from '../UserSettingsStore'; import SettingsStore from "../settings/SettingsStore";
import EmojiData from '../stripped-emoji.json'; import EmojiData from '../stripped-emoji.json';
@ -96,7 +96,7 @@ export default class EmojiProvider extends AutocompleteProvider {
} }
async getCompletions(query: string, selection: SelectionRange) { async getCompletions(query: string, selection: SelectionRange) {
if (UserSettingsStore.getSyncedSetting("MessageComposerInput.dontSuggestEmoji")) { if (SettingsStore.getValue("MessageComposerInput.dontSuggestEmoji")) {
return []; // don't give any suggestions if the user doesn't want them return []; // don't give any suggestions if the user doesn't want them
} }

View file

@ -53,9 +53,11 @@ export default class UserProvider extends AutocompleteProvider {
} }
destroy() { destroy() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound); MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound);
MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound); MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound);
} }
}
_onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { _onRoomTimeline(ev, room, toStartOfTimeline, removed, data) {
if (!room) return; if (!room) return;

View file

@ -33,6 +33,7 @@ module.exports = {
menuHeight: React.PropTypes.number, menuHeight: React.PropTypes.number,
chevronOffset: React.PropTypes.number, chevronOffset: React.PropTypes.number,
menuColour: React.PropTypes.string, menuColour: React.PropTypes.string,
chevronFace: React.PropTypes.string, // top, bottom, left, right
}, },
getOrCreateContainer: function() { getOrCreateContainer: function() {
@ -58,12 +59,30 @@ module.exports = {
} }
}; };
const position = { const position = {};
top: props.top, let chevronFace = null;
};
if (props.top) {
position.top = props.top;
} else {
position.bottom = props.bottom;
}
if (props.left) {
position.left = props.left;
chevronFace = 'left';
} else {
position.right = props.right;
chevronFace = 'right';
}
const chevronOffset = {}; const chevronOffset = {};
if (props.chevronOffset) { if (props.chevronFace) {
chevronFace = props.chevronFace;
}
if (chevronFace === 'top' || chevronFace === 'bottom') {
chevronOffset.left = props.chevronOffset;
} else {
chevronOffset.top = props.chevronOffset; chevronOffset.top = props.chevronOffset;
} }
@ -74,28 +93,27 @@ module.exports = {
.mx_ContextualMenu_chevron_left:after { .mx_ContextualMenu_chevron_left:after {
border-right-color: ${props.menuColour}; border-right-color: ${props.menuColour};
} }
.mx_ContextualMenu_chevron_right:after { .mx_ContextualMenu_chevron_right:after {
border-left-color: ${props.menuColour}; border-left-color: ${props.menuColour};
} }
.mx_ContextualMenu_chevron_top:after {
border-left-color: ${props.menuColour};
}
.mx_ContextualMenu_chevron_bottom:after {
border-left-color: ${props.menuColour};
}
`; `;
} }
let chevron = null; const chevron = <div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace}></div>;
if (props.left) {
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>;
position.left = props.left;
} else {
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div>;
position.right = props.right;
}
const className = 'mx_ContextualMenu_wrapper'; const className = 'mx_ContextualMenu_wrapper';
const menuClasses = classNames({ const menuClasses = classNames({
'mx_ContextualMenu': true, 'mx_ContextualMenu': true,
'mx_ContextualMenu_left': props.left, 'mx_ContextualMenu_left': chevronFace === 'left',
'mx_ContextualMenu_right': !props.left, 'mx_ContextualMenu_right': chevronFace === 'right',
'mx_ContextualMenu_top': chevronFace === 'top',
'mx_ContextualMenu_bottom': chevronFace === 'bottom',
}); });
const menuStyle = {}; const menuStyle = {};

View file

@ -19,7 +19,7 @@ import React from 'react';
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import sdk from '../../index'; import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg'; import MatrixClientPeg from '../../MatrixClientPeg';
import { _t, _tJsx } from '../../languageHandler'; import { _t } from '../../languageHandler';
/* /*
* Component which shows the filtered file using a TimelinePanel * Component which shows the filtered file using a TimelinePanel
@ -92,7 +92,10 @@ const FilePanel = React.createClass({
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper"> return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
<div className="mx_RoomView_empty"> <div className="mx_RoomView_empty">
{ _tJsx("You must <a>register</a> to use this functionality", /<a>(.*?)<\/a>/, (sub) => <a href="#/register" key="sub">{ sub }</a>) } { _t("You must <a>register</a> to use this functionality",
{},
{ 'a': (sub) => <a href="#/register" key="sub">{ sub }</a> })
}
</div> </div>
</div>; </div>;
} else if (this.noRoom) { } else if (this.noRoom) {

View file

@ -22,7 +22,7 @@ import MatrixClientPeg from '../../MatrixClientPeg';
import sdk from '../../index'; import sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import { sanitizedHtmlNode } from '../../HtmlUtils'; import { sanitizedHtmlNode } from '../../HtmlUtils';
import { _t, _td, _tJsx } from '../../languageHandler'; import { _t, _td } from '../../languageHandler';
import AccessibleButton from '../views/elements/AccessibleButton'; import AccessibleButton from '../views/elements/AccessibleButton';
import Modal from '../../Modal'; import Modal from '../../Modal';
import classnames from 'classnames'; import classnames from 'classnames';
@ -469,7 +469,7 @@ export default React.createClass({
if (group && group.inviter && group.inviter.userId) { if (group && group.inviter && group.inviter.userId) {
this._fetchInviterProfile(group.inviter.userId); this._fetchInviterProfile(group.inviter.userId);
} }
this._groupStore = GroupStoreCache.getGroupStore(this._matrixClient, groupId); this._groupStore = GroupStoreCache.getGroupStore(groupId);
this._groupStore.registerListener(() => { this._groupStore.registerListener(() => {
const summary = this._groupStore.getSummary(); const summary = this._groupStore.getSummary();
if (summary.profile) { if (summary.profile) {
@ -495,7 +495,19 @@ export default React.createClass({
this._onEditClick(); this._onEditClick();
} }
}); });
let willDoOnboarding = false;
this._groupStore.on('error', (err) => { this._groupStore.on('error', (err) => {
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN' && !willDoOnboarding) {
dis.dispatch({
action: 'do_after_sync_prepared',
deferred_action: {
action: 'view_group',
group_id: groupId,
},
});
dis.dispatch({action: 'view_set_mxid'});
willDoOnboarding = true;
}
this.setState({ this.setState({
summary: null, summary: null,
error: err, error: err,
@ -672,18 +684,6 @@ export default React.createClass({
showGroupAddRoomDialog(this.props.groupId); showGroupAddRoomDialog(this.props.groupId);
}, },
_onPublicityToggle: function() {
this.setState({
publicityBusy: true,
});
const publicity = !this.state.isGroupPublicised;
this._groupStore.setGroupPublicity(publicity).then(() => {
this.setState({
publicityBusy: false,
});
});
},
_getGroupSection: function() { _getGroupSection: function() {
const groupSettingsSectionClasses = classnames({ const groupSettingsSectionClasses = classnames({
"mx_GroupView_group": this.state.editing, "mx_GroupView_group": this.state.editing,
@ -903,25 +903,6 @@ export default React.createClass({
return null; return null;
}, },
_getMemberSettingsSection: function() {
return <div className="mx_GroupView_memberSettings">
<h2> { _t("Community Member Settings") } </h2>
<div className="mx_GroupView_memberSettings_toggle">
<input type="checkbox"
onClick={this._onPublicityToggle}
checked={this.state.isGroupPublicised}
tabIndex="3"
id="isGroupPublicised"
/>
<label htmlFor="isGroupPublicised"
onClick={this._onPublicityToggle}
>
{ _t("Publish this community on your profile") }
</label>
</div>
</div>;
},
_getLongDescriptionNode: function() { _getLongDescriptionNode: function() {
const summary = this.state.summary; const summary = this.state.summary;
let description = null; let description = null;
@ -932,12 +913,12 @@ export default React.createClass({
className="mx_GroupView_groupDesc_placeholder" className="mx_GroupView_groupDesc_placeholder"
onClick={this._onEditClick} onClick={this._onEditClick}
> >
{ _tJsx( { _t(
'Your community hasn\'t got a Long Description, a HTML page to show to community members.<br />' + 'Your community hasn\'t got a Long Description, a HTML page to show to community members.<br />' +
'Click here to open settings and give it one!', 'Click here to open settings and give it one!',
[/<br \/>/], {},
[(sub) => <br />]) { 'br': <br /> },
} ) }
</div>; </div>;
} }
const groupDescEditingClasses = classnames({ const groupDescEditingClasses = classnames({
@ -976,7 +957,6 @@ export default React.createClass({
let shortDescNode; let shortDescNode;
const bodyNodes = [ const bodyNodes = [
this._getMembershipSection(), this._getMembershipSection(),
this.state.editing ? this._getMemberSettingsSection() : null,
this._getGroupSection(), this._getGroupSection(),
]; ];
const rightButtons = []; const rightButtons = [];

View file

@ -18,9 +18,10 @@ limitations under the License.
import * as Matrix from 'matrix-js-sdk'; import * as Matrix from 'matrix-js-sdk';
import React from 'react'; import React from 'react';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import UserSettingsStore from '../../UserSettingsStore'; import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
import KeyCode from '../../KeyCode';
import Notifier from '../../Notifier'; import Notifier from '../../Notifier';
import PageTypes from '../../PageTypes'; import PageTypes from '../../PageTypes';
import CallMediaHandler from '../../CallMediaHandler'; import CallMediaHandler from '../../CallMediaHandler';
@ -28,6 +29,7 @@ import sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import sessionStore from '../../stores/SessionStore'; import sessionStore from '../../stores/SessionStore';
import MatrixClientPeg from '../../MatrixClientPeg'; import MatrixClientPeg from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore";
/** /**
* This is what our MatrixChat shows when we are logged in. The precise view is * This is what our MatrixChat shows when we are logged in. The precise view is
@ -38,7 +40,7 @@ import MatrixClientPeg from '../../MatrixClientPeg';
* *
* Components mounted below us can access the matrix client via the react context. * Components mounted below us can access the matrix client via the react context.
*/ */
export default React.createClass({ const LoggedInView = React.createClass({
displayName: 'LoggedInView', displayName: 'LoggedInView',
propTypes: { propTypes: {
@ -74,7 +76,7 @@ export default React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
// use compact timeline view // use compact timeline view
useCompactLayout: UserSettingsStore.getSyncedSetting('useCompactLayout'), useCompactLayout: SettingsStore.getValue('useCompactLayout'),
}; };
}, },
@ -153,13 +155,7 @@ export default React.createClass({
*/ */
let handled = false; let handled = false;
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
let ctrlCmdOnly;
if (isMac) {
ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
} else {
ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
}
switch (ev.keyCode) { switch (ev.keyCode) {
case KeyCode.UP: case KeyCode.UP:
@ -213,6 +209,7 @@ export default React.createClass({
}, },
render: function() { render: function() {
const TagPanel = sdk.getComponent('structures.TagPanel');
const LeftPanel = sdk.getComponent('structures.LeftPanel'); const LeftPanel = sdk.getComponent('structures.LeftPanel');
const RightPanel = sdk.getComponent('structures.RightPanel'); const RightPanel = sdk.getComponent('structures.RightPanel');
const RoomView = sdk.getComponent('structures.RoomView'); const RoomView = sdk.getComponent('structures.RoomView');
@ -334,6 +331,7 @@ export default React.createClass({
<div className='mx_MatrixChat_wrapper'> <div className='mx_MatrixChat_wrapper'>
{ topBar } { topBar }
<div className={bodyClasses}> <div className={bodyClasses}>
{ SettingsStore.isFeatureEnabled("feature_tag_panel") ? <TagPanel /> : <div /> }
<LeftPanel <LeftPanel
selectedRoom={this.props.currentRoomId} selectedRoom={this.props.currentRoomId}
collapsed={this.props.collapseLhs || false} collapsed={this.props.collapseLhs || false}
@ -348,3 +346,5 @@ export default React.createClass({
); );
}, },
}); });
export default DragDropContext(HTML5Backend)(LoggedInView);

View file

@ -22,7 +22,6 @@ import React from 'react';
import Matrix from "matrix-js-sdk"; import Matrix from "matrix-js-sdk";
import Analytics from "../../Analytics"; import Analytics from "../../Analytics";
import UserSettingsStore from '../../UserSettingsStore';
import MatrixClientPeg from "../../MatrixClientPeg"; import MatrixClientPeg from "../../MatrixClientPeg";
import PlatformPeg from "../../PlatformPeg"; import PlatformPeg from "../../PlatformPeg";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
@ -41,9 +40,9 @@ require('../../stores/LifecycleStore');
import PageTypes from '../../PageTypes'; import PageTypes from '../../PageTypes';
import createRoom from "../../createRoom"; import createRoom from "../../createRoom";
import * as UDEHandler from '../../UnknownDeviceErrorHandler';
import KeyRequestHandler from '../../KeyRequestHandler'; import KeyRequestHandler from '../../KeyRequestHandler';
import { _t, getCurrentLanguage } from '../../languageHandler'; import { _t, getCurrentLanguage } from '../../languageHandler';
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
const VIEWS = { const VIEWS = {
@ -81,11 +80,10 @@ const ONBOARDING_FLOW_STARTERS = [
'view_user_settings', 'view_user_settings',
'view_create_chat', 'view_create_chat',
'view_create_room', 'view_create_room',
'view_my_groups', 'view_create_group',
'view_group',
]; ];
module.exports = React.createClass({ export default React.createClass({
// we export this so that the integration tests can use it :-S // we export this so that the integration tests can use it :-S
statics: { statics: {
VIEWS: VIEWS, VIEWS: VIEWS,
@ -224,7 +222,7 @@ module.exports = React.createClass({
componentWillMount: function() { componentWillMount: function() {
SdkConfig.put(this.props.config); SdkConfig.put(this.props.config);
if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable(); if (!SettingsStore.getValue("analyticsOptOut")) Analytics.enable();
// Used by _viewRoom before getting state from sync // Used by _viewRoom before getting state from sync
this.firstSyncComplete = false; this.firstSyncComplete = false;
@ -287,11 +285,15 @@ module.exports = React.createClass({
this._windowWidth = 10000; this._windowWidth = 10000;
this.handleResize(); this.handleResize();
window.addEventListener('resize', this.handleResize); window.addEventListener('resize', this.handleResize);
// check we have the right tint applied for this theme.
// N.B. we don't call the whole of setTheme() here as we may be
// racing with the theme CSS download finishing from index.js
Tinter.tint();
}, },
componentDidMount: function() { componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
UDEHandler.startListening();
this.focusComposer = false; this.focusComposer = false;
@ -357,7 +359,6 @@ module.exports = React.createClass({
componentWillUnmount: function() { componentWillUnmount: function() {
Lifecycle.stopMatrixClient(); Lifecycle.stopMatrixClient();
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
UDEHandler.stopListening();
window.removeEventListener("focus", this.onFocus); window.removeEventListener("focus", this.onFocus);
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
}, },
@ -496,6 +497,11 @@ module.exports = React.createClass({
case 'view_create_room': case 'view_create_room':
this._createRoom(); this._createRoom();
break; break;
case 'view_create_group': {
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
Modal.createTrackedDialog('Create Community', '', CreateGroupDialog);
}
break;
case 'view_room_directory': case 'view_room_directory':
this._setPage(PageTypes.RoomDirectory); this._setPage(PageTypes.RoomDirectory);
this.notifyNewScreen('directory'); this.notifyNewScreen('directory');
@ -586,6 +592,9 @@ module.exports = React.createClass({
this._onWillStartClient(); this._onWillStartClient();
}); });
break; break;
case 'client_started':
this._onClientStarted();
break;
case 'new_version': case 'new_version':
this.onVersion( this.onVersion(
payload.currentVersion, payload.newVersion, payload.currentVersion, payload.newVersion,
@ -883,7 +892,7 @@ module.exports = React.createClass({
*/ */
_onSetTheme: function(theme) { _onSetTheme: function(theme) {
if (!theme) { if (!theme) {
theme = 'light'; theme = SettingsStore.getValue("theme");
} }
// look for the stylesheet elements. // look for the stylesheet elements.
@ -906,18 +915,49 @@ module.exports = React.createClass({
// disable all of them first, then enable the one we want. Chrome only // disable all of them first, then enable the one we want. Chrome only
// bothers to do an update on a true->false transition, so this ensures // bothers to do an update on a true->false transition, so this ensures
// that we get exactly one update, at the right time. // that we get exactly one update, at the right time.
//
// ^ This comment was true when we used to use alternative stylesheets
// for the CSS. Nowadays we just set them all as disabled in index.html
// and enable them as needed. It might be cleaner to disable them all
// at the same time to prevent loading two themes simultaneously and
// having them interact badly... but this causes a flash of unstyled app
// which is even uglier. So we don't.
Object.values(styleElements).forEach((a) => {
a.disabled = true;
});
styleElements[theme].disabled = false; styleElements[theme].disabled = false;
if (theme === 'dark') { const switchTheme = function() {
// abuse the tinter to change all the SVG's #fff to #2d2d2d // we re-enable our theme here just in case we raced with another
// XXX: obviously this shouldn't be hardcoded here. // theme set request as per https://github.com/vector-im/riot-web/issues/5601.
Tinter.tintSvgWhite('#2d2d2d'); // We could alternatively lock or similar to stop the race, but
} else { // this is probably good enough for now.
Tinter.tintSvgWhite('#ffffff'); styleElements[theme].disabled = false;
Object.values(styleElements).forEach((a) => {
if (a == styleElements[theme]) return;
a.disabled = true;
});
Tinter.setTheme(theme);
};
// turns out that Firefox preloads the CSS for link elements with
// the disabled attribute, but Chrome doesn't.
let cssLoaded = false;
styleElements[theme].onload = () => {
switchTheme();
};
for (let i = 0; i < document.styleSheets.length; i++) {
const ss = document.styleSheets[i];
if (ss && ss.href === styleElements[theme].href) {
cssLoaded = true;
break;
}
}
if (cssLoaded) {
styleElements[theme].onload = undefined;
switchTheme();
} }
}, },
@ -1025,10 +1065,10 @@ module.exports = React.createClass({
// this if we are not scrolled up in the view. To find out, delegate to // this if we are not scrolled up in the view. To find out, delegate to
// the timeline panel. If the timeline panel doesn't exist, then we assume // the timeline panel. If the timeline panel doesn't exist, then we assume
// it is safe to reset the timeline. // it is safe to reset the timeline.
if (!self.refs.loggedInView) { if (!self._loggedInView) {
return true; return true;
} }
return self.refs.loggedInView.canResetTimelineInRoom(roomId); return self._loggedInView.getDecoratedComponentInstance().canResetTimelineInRoom(roomId);
}); });
cli.on('sync', function(state, prevState) { cli.on('sync', function(state, prevState) {
@ -1088,6 +1128,65 @@ module.exports = React.createClass({
cli.on("crypto.roomKeyRequestCancellation", (req) => { cli.on("crypto.roomKeyRequestCancellation", (req) => {
krh.handleKeyRequestCancellation(req); krh.handleKeyRequestCancellation(req);
}); });
cli.on("Room", (room) => {
if (MatrixClientPeg.get().isCryptoEnabled()) {
const blacklistEnabled = SettingsStore.getValueAt(
SettingLevel.ROOM_DEVICE,
"blacklistUnverifiedDevices",
room.roomId,
/*explicit=*/true,
);
room.setBlacklistUnverifiedDevices(blacklistEnabled);
}
});
cli.on("crypto.warning", (type) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
switch (type) {
case 'CRYPTO_WARNING_ACCOUNT_MIGRATED':
Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, {
title: _t('Cryptography data migrated'),
description: _t(
"A one-off migration of cryptography data has been performed. "+
"End-to-end encryption will not work if you go back to an older "+
"version of Riot. If you need to use end-to-end cryptography on "+
"an older version, log out of Riot first. To retain message history, "+
"export and re-import your keys.",
),
});
break;
case 'CRYPTO_WARNING_OLD_VERSION_DETECTED':
Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, {
title: _t('Old cryptography data detected'),
description: _t(
"Data from an older version of Riot has been detected. "+
"This will have caused end-to-end cryptography to malfunction "+
"in the older version. End-to-end encrypted messages exchanged "+
"recently whilst using the older version may not be decryptable "+
"in this version. This may also cause messages exchanged with this "+
"version to fail. If you experience problems, log out and back in "+
"again. To retain message history, export and re-import your keys.",
),
});
break;
}
});
},
/**
* Called shortly after the matrix client has started. Useful for
* setting up anything that requires the client to be started.
* @private
*/
_onClientStarted: function() {
const cli = MatrixClientPeg.get();
if (cli.isCryptoEnabled()) {
const blacklistEnabled = SettingsStore.getValueAt(
SettingLevel.DEVICE,
"blacklistUnverifiedDevices",
);
cli.setGlobalBlacklistUnverifiedDevices(blacklistEnabled);
}
}, },
showScreen: function(screen, params) { showScreen: function(screen, params) {
@ -1327,13 +1426,6 @@ module.exports = React.createClass({
cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => { cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => {
dis.dispatch({action: 'message_sent'}); dis.dispatch({action: 'message_sent'});
}, (err) => { }, (err) => {
if (err.name === 'UnknownDeviceError') {
dis.dispatch({
action: 'unknown_device_error',
err: err,
room: cli.getRoom(roomId),
});
}
dis.dispatch({action: 'message_send_failed'}); dis.dispatch({action: 'message_send_failed'});
}); });
}, },
@ -1395,6 +1487,10 @@ module.exports = React.createClass({
return this.props.makeRegistrationUrl(params); return this.props.makeRegistrationUrl(params);
}, },
_collectLoggedInView: function(ref) {
this._loggedInView = ref;
},
render: function() { render: function() {
// console.log(`Rendering MatrixChat with view ${this.state.view}`); // console.log(`Rendering MatrixChat with view ${this.state.view}`);
@ -1427,7 +1523,7 @@ module.exports = React.createClass({
*/ */
const LoggedInView = sdk.getComponent('structures.LoggedInView'); const LoggedInView = sdk.getComponent('structures.LoggedInView');
return ( return (
<LoggedInView ref="loggedInView" matrixClient={MatrixClientPeg.get()} <LoggedInView ref={this._collectLoggedInView} matrixClient={MatrixClientPeg.get()}
onRoomCreated={this.onRoomCreated} onRoomCreated={this.onRoomCreated}
onUserSettingsClose={this.onUserSettingsClose} onUserSettingsClose={this.onUserSettingsClose}
onRegistered={this.onRegistered} onRegistered={this.onRegistered}

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import UserSettingsStore from '../../UserSettingsStore';
import shouldHideEvent from '../../shouldHideEvent'; import shouldHideEvent from '../../shouldHideEvent';
import dis from "../../dispatcher"; import dis from "../../dispatcher";
import sdk from '../../index'; import sdk from '../../index';
@ -110,8 +109,6 @@ module.exports = React.createClass({
// Velocity requires // Velocity requires
this._readMarkerGhostNode = null; this._readMarkerGhostNode = null;
this._syncedSettings = UserSettingsStore.getSyncedSettings();
this._isMounted = true; this._isMounted = true;
}, },
@ -251,7 +248,7 @@ module.exports = React.createClass({
// Always show highlighted event // Always show highlighted event
if (this.props.highlightedEventId === mxEv.getId()) return true; if (this.props.highlightedEventId === mxEv.getId()) return true;
return !shouldHideEvent(mxEv, this._syncedSettings); return !shouldHideEvent(mxEv);
}, },
_getEventTiles: function() { _getEventTiles: function() {

View file

@ -15,69 +15,12 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import GeminiScrollbar from 'react-gemini-scrollbar'; import GeminiScrollbar from 'react-gemini-scrollbar';
import {MatrixClient} from 'matrix-js-sdk';
import sdk from '../../index'; import sdk from '../../index';
import { _t, _tJsx } from '../../languageHandler'; import { _t } from '../../languageHandler';
import dis from '../../dispatcher';
import withMatrixClient from '../../wrappers/withMatrixClient'; import withMatrixClient from '../../wrappers/withMatrixClient';
import AccessibleButton from '../views/elements/AccessibleButton'; import AccessibleButton from '../views/elements/AccessibleButton';
import dis from '../../dispatcher';
import Modal from '../../Modal';
import FlairStore from '../../stores/FlairStore';
const GroupTile = React.createClass({
displayName: 'GroupTile',
propTypes: {
groupId: PropTypes.string.isRequired,
},
contextTypes: {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
},
getInitialState() {
return {
profile: null,
};
},
componentWillMount: function() {
FlairStore.getGroupProfileCached(this.context.matrixClient, this.props.groupId).then((profile) => {
this.setState({profile});
});
},
onClick: function(e) {
e.preventDefault();
dis.dispatch({
action: 'view_group',
group_id: this.props.groupId,
});
},
render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const profile = this.state.profile || {};
const name = profile.name || this.props.groupId;
const desc = profile.shortDescription;
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
profile.avatarUrl, 50, 50, "crop",
) : null;
return <AccessibleButton className="mx_GroupTile" onClick={this.onClick}>
<div className="mx_GroupTile_avatar">
<BaseAvatar name={name} url={httpUrl} width={50} height={50} />
</div>
<div className="mx_GroupTile_profile">
<h3 className="mx_GroupTile_name">{ name }</h3>
<div className="mx_GroupTile_desc">{ desc }</div>
<div className="mx_GroupTile_groupId">{ this.props.groupId }</div>
</div>
</AccessibleButton>;
},
});
export default withMatrixClient(React.createClass({ export default withMatrixClient(React.createClass({
displayName: 'MyGroups', displayName: 'MyGroups',
@ -98,14 +41,18 @@ export default withMatrixClient(React.createClass({
}, },
_onCreateGroupClick: function() { _onCreateGroupClick: function() {
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog"); dis.dispatch({action: 'view_create_group'});
Modal.createTrackedDialog('Create Community', '', CreateGroupDialog);
}, },
_fetch: function() { _fetch: function() {
this.props.matrixClient.getJoinedGroups().done((result) => { this.props.matrixClient.getJoinedGroups().done((result) => {
this.setState({groups: result.groups, error: null}); this.setState({groups: result.groups, error: null});
}, (err) => { }, (err) => {
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
// Indicate that the guest isn't in any groups (which should be true)
this.setState({groups: [], error: null});
return;
}
this.setState({groups: null, error: err}); this.setState({groups: null, error: err});
}); });
}, },
@ -114,6 +61,7 @@ export default withMatrixClient(React.createClass({
const Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader'); const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
const TintableSvg = sdk.getComponent("elements.TintableSvg"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
const GroupTile = sdk.getComponent("groups.GroupTile");
let content; let content;
let contentHeader; let contentHeader;
@ -165,13 +113,13 @@ export default withMatrixClient(React.createClass({
<div className="mx_MyGroups_headerCard_header"> <div className="mx_MyGroups_headerCard_header">
{ _t('Join an existing community') } { _t('Join an existing community') }
</div> </div>
{ _tJsx( { _t(
'To join an existing community you\'ll have to '+ 'To join an existing community you\'ll have to '+
'know its community identifier; this will look '+ 'know its community identifier; this will look '+
'something like <i>+example:matrix.org</i>.', 'something like <i>+example:matrix.org</i>.',
/<i>(.*)<\/i>/, {},
(sub) => <i>{ sub }</i>, { 'i': (sub) => <i>{ sub }</i> })
) } }
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 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.
@ -15,17 +16,26 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import { _t, _tJsx } from '../../languageHandler'; import Matrix from 'matrix-js-sdk';
import { _t } from '../../languageHandler';
import sdk from '../../index'; import sdk from '../../index';
import WhoIsTyping from '../../WhoIsTyping'; import WhoIsTyping from '../../WhoIsTyping';
import MatrixClientPeg from '../../MatrixClientPeg'; import MatrixClientPeg from '../../MatrixClientPeg';
import MemberAvatar from '../views/avatars/MemberAvatar'; import MemberAvatar from '../views/avatars/MemberAvatar';
import Resend from '../../Resend';
import { showUnknownDeviceDialogForMessages } from '../../cryptodevices';
const HIDE_DEBOUNCE_MS = 10000;
const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED = 1;
const STATUS_BAR_EXPANDED_LARGE = 2; const STATUS_BAR_EXPANDED_LARGE = 2;
function getUnsentMessages(room) {
if (!room) { return []; }
return room.getPendingEvents().filter(function(ev) {
return ev.status === Matrix.EventStatus.NOT_SENT;
});
};
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RoomStatusBar', displayName: 'RoomStatusBar',
@ -36,9 +46,6 @@ module.exports = React.createClass({
// the number of messages which have arrived since we've been scrolled up // the number of messages which have arrived since we've been scrolled up
numUnreadMessages: React.PropTypes.number, numUnreadMessages: React.PropTypes.number,
// string to display when there are messages in the room which had errors on send
unsentMessageError: React.PropTypes.string,
// this is true if we are fully scrolled-down, and are looking at // this is true if we are fully scrolled-down, and are looking at
// the end of the live timeline. // the end of the live timeline.
atEndOfLiveTimeline: React.PropTypes.bool, atEndOfLiveTimeline: React.PropTypes.bool,
@ -99,12 +106,14 @@ module.exports = React.createClass({
return { return {
syncState: MatrixClientPeg.get().getSyncState(), syncState: MatrixClientPeg.get().getSyncState(),
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room), usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
unsentMessages: getUnsentMessages(this.props.room),
}; };
}, },
componentWillMount: function() { componentWillMount: function() {
MatrixClientPeg.get().on("sync", this.onSyncStateChange); MatrixClientPeg.get().on("sync", this.onSyncStateChange);
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping); MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
MatrixClientPeg.get().on("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
this._checkSize(); this._checkSize();
}, },
@ -119,6 +128,7 @@ module.exports = React.createClass({
if (client) { if (client) {
client.removeListener("sync", this.onSyncStateChange); client.removeListener("sync", this.onSyncStateChange);
client.removeListener("RoomMember.typing", this.onRoomMemberTyping); client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
client.removeListener("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
} }
}, },
@ -137,6 +147,26 @@ module.exports = React.createClass({
}); });
}, },
_onResendAllClick: function() {
Resend.resendUnsentEvents(this.props.room);
},
_onCancelAllClick: function() {
Resend.cancelUnsentEvents(this.props.room);
},
_onShowDevicesClick: function() {
showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room);
},
_onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) {
if (room.roomId !== this.props.room.roomId) return;
this.setState({
unsentMessages: getUnsentMessages(this.props.room),
});
},
// Check whether current size is greater than 0, if yes call props.onVisible // Check whether current size is greater than 0, if yes call props.onVisible
_checkSize: function() { _checkSize: function() {
if (this.props.onVisible && this._getSize()) { if (this.props.onVisible && this._getSize()) {
@ -156,7 +186,7 @@ module.exports = React.createClass({
this.props.sentMessageAndIsAlone this.props.sentMessageAndIsAlone
) { ) {
return STATUS_BAR_EXPANDED; return STATUS_BAR_EXPANDED;
} else if (this.props.unsentMessageError) { } else if (this.state.unsentMessages.length > 0) {
return STATUS_BAR_EXPANDED_LARGE; return STATUS_BAR_EXPANDED_LARGE;
} }
return STATUS_BAR_HIDDEN; return STATUS_BAR_HIDDEN;
@ -242,6 +272,61 @@ module.exports = React.createClass({
return avatars; return avatars;
}, },
_getUnsentMessageContent: function() {
const unsentMessages = this.state.unsentMessages;
if (!unsentMessages.length) return null;
let title;
let content;
const hasUDE = unsentMessages.some((m) => {
return m.error && m.error.name === "UnknownDeviceError";
});
if (hasUDE) {
title = _t("Message not sent due to unknown devices being present");
content = _t(
"<showDevicesText>Show devices</showDevicesText> or <cancelText>cancel all</cancelText>.",
{},
{
'showDevicesText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
'cancelText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
},
);
} else {
if (
unsentMessages.length === 1 &&
unsentMessages[0].error &&
unsentMessages[0].error.data &&
unsentMessages[0].error.data.error
) {
title = unsentMessages[0].error.data.error;
} else {
title = _t("Some of your messages have not been sent.");
}
content = _t("<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
"You can also select individual messages to resend or cancel.",
{},
{
'resendText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>,
'cancelText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
},
);
}
return <div className="mx_RoomStatusBar_connectionLostBar">
<img src="img/warning.svg" width="24" height="23" title={_t("Warning")} alt={_t("Warning")} />
<div className="mx_RoomStatusBar_connectionLostBar_title">
{ title }
</div>
<div className="mx_RoomStatusBar_connectionLostBar_desc">
{ content }
</div>
</div>;
},
// return suitable content for the main (text) part of the status bar. // return suitable content for the main (text) part of the status bar.
_getContent: function() { _getContent: function() {
const EmojiText = sdk.getComponent('elements.EmojiText'); const EmojiText = sdk.getComponent('elements.EmojiText');
@ -264,24 +349,8 @@ module.exports = React.createClass({
); );
} }
if (this.props.unsentMessageError) { if (this.state.unsentMessages.length > 0) {
return ( return this._getUnsentMessageContent();
<div className="mx_RoomStatusBar_connectionLostBar">
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ " />
<div className="mx_RoomStatusBar_connectionLostBar_title">
{ this.props.unsentMessageError }
</div>
<div className="mx_RoomStatusBar_connectionLostBar_desc">
{ _tJsx("<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.",
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
[
(sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this.props.onResendAllClick}>{ sub }</a>,
(sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this.props.onCancelAllClick}>{ sub }</a>,
],
) }
</div>
</div>
);
} }
// unread count trumps who is typing since the unread count is only // unread count trumps who is typing since the unread count is only
@ -322,12 +391,15 @@ module.exports = React.createClass({
if (this.props.sentMessageAndIsAlone) { if (this.props.sentMessageAndIsAlone) {
return ( return (
<div className="mx_RoomStatusBar_isAlone"> <div className="mx_RoomStatusBar_isAlone">
{ _tJsx("There's no one else here! Would you like to <a>invite others</a> or <a>stop warning about the empty room</a>?", { _t("There's no one else here! Would you like to <inviteText>invite others</inviteText> " +
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/], "or <nowarnText>stop warning about the empty room</nowarnText>?",
[ {},
(sub) => <a className="mx_RoomStatusBar_resend_link" key="invite" onClick={this.props.onInviteClick}>{ sub }</a>, {
(sub) => <a className="mx_RoomStatusBar_resend_link" key="nowarn" onClick={this.props.onStopWarningClick}>{ sub }</a>, 'inviteText': (sub) =>
], <a className="mx_RoomStatusBar_resend_link" key="invite" onClick={this.props.onInviteClick}>{ sub }</a>,
'nowarnText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="nowarn" onClick={this.props.onStopWarningClick}>{ sub }</a>,
},
) } ) }
</div> </div>
); );
@ -336,7 +408,6 @@ module.exports = React.createClass({
return null; return null;
}, },
render: function() { render: function() {
const content = this._getContent(); const content = this._getContent();
const indicator = this._getIndicator(this.state.usersTyping.length > 0); const indicator = this._getIndicator(this.state.usersTyping.length > 0);

View file

@ -26,26 +26,24 @@ const React = require("react");
const ReactDOM = require("react-dom"); const ReactDOM = require("react-dom");
import Promise from 'bluebird'; import Promise from 'bluebird';
const classNames = require("classnames"); const classNames = require("classnames");
const Matrix = require("matrix-js-sdk");
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
const UserSettingsStore = require('../../UserSettingsStore');
const MatrixClientPeg = require("../../MatrixClientPeg"); const MatrixClientPeg = require("../../MatrixClientPeg");
const ContentMessages = require("../../ContentMessages"); const ContentMessages = require("../../ContentMessages");
const Modal = require("../../Modal"); const Modal = require("../../Modal");
const sdk = require('../../index'); const sdk = require('../../index');
const CallHandler = require('../../CallHandler'); const CallHandler = require('../../CallHandler');
const Resend = require("../../Resend");
const dis = require("../../dispatcher"); const dis = require("../../dispatcher");
const Tinter = require("../../Tinter"); const Tinter = require("../../Tinter");
const rate_limited_func = require('../../ratelimitedfunc'); const rate_limited_func = require('../../ratelimitedfunc');
const ObjectUtils = require('../../ObjectUtils'); const ObjectUtils = require('../../ObjectUtils');
const Rooms = require('../../Rooms'); const Rooms = require('../../Rooms');
import KeyCode from '../../KeyCode'; import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
import RoomViewStore from '../../stores/RoomViewStore'; import RoomViewStore from '../../stores/RoomViewStore';
import RoomScrollStateStore from '../../stores/RoomScrollStateStore'; import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
import SettingsStore from "../../settings/SettingsStore";
const DEBUG = false; const DEBUG = false;
let debuglog = function() {}; let debuglog = function() {};
@ -110,7 +108,6 @@ module.exports = React.createClass({
draggingFile: false, draggingFile: false,
searching: false, searching: false,
searchResults: null, searchResults: null,
unsentMessageError: '',
callState: null, callState: null,
guestsCanJoin: false, guestsCanJoin: false,
canPeek: false, canPeek: false,
@ -149,8 +146,6 @@ module.exports = React.createClass({
MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership); MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership);
MatrixClientPeg.get().on("accountData", this.onAccountData); MatrixClientPeg.get().on("accountData", this.onAccountData);
this._syncedSettings = UserSettingsStore.getSyncedSettings();
// Start listening for RoomViewStore updates // Start listening for RoomViewStore updates
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._onRoomViewStoreUpdate(true); this._onRoomViewStoreUpdate(true);
@ -204,7 +199,6 @@ module.exports = React.createClass({
if (initial) { if (initial) {
newState.room = MatrixClientPeg.get().getRoom(newState.roomId); newState.room = MatrixClientPeg.get().getRoom(newState.roomId);
if (newState.room) { if (newState.room) {
newState.unsentMessageError = this._getUnsentMessageError(newState.room);
newState.showApps = this._shouldShowApps(newState.room); newState.showApps = this._shouldShowApps(newState.room);
this._onRoomLoaded(newState.room); this._onRoomLoaded(newState.room);
} }
@ -305,7 +299,7 @@ module.exports = React.createClass({
// Check if user has previously chosen to hide the app drawer for this // Check if user has previously chosen to hide the app drawer for this
// room. If so, do not show apps // room. If so, do not show apps
let hideWidgetDrawer = localStorage.getItem( const hideWidgetDrawer = localStorage.getItem(
room.roomId + "_hide_widget_drawer"); room.roomId + "_hide_widget_drawer");
if (hideWidgetDrawer === "true") { if (hideWidgetDrawer === "true") {
@ -435,13 +429,7 @@ module.exports = React.createClass({
onKeyDown: function(ev) { onKeyDown: function(ev) {
let handled = false; let handled = false;
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
let ctrlCmdOnly;
if (isMac) {
ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
} else {
ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
}
switch (ev.keyCode) { switch (ev.keyCode) {
case KeyCode.KEY_D: case KeyCode.KEY_D:
@ -470,11 +458,6 @@ module.exports = React.createClass({
case 'message_send_failed': case 'message_send_failed':
case 'message_sent': case 'message_sent':
this._checkIfAlone(this.state.room); this._checkIfAlone(this.state.room);
// no break; to intentionally fall through
case 'message_send_cancelled':
this.setState({
unsentMessageError: this._getUnsentMessageError(this.state.room),
});
break; break;
case 'notifier_enabled': case 'notifier_enabled':
case 'upload_failed': case 'upload_failed':
@ -542,7 +525,7 @@ module.exports = React.createClass({
// update unread count when scrolled up // update unread count when scrolled up
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) { if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
// no change // no change
} else if (!shouldHideEvent(ev, this._syncedSettings)) { } else if (!shouldHideEvent(ev)) {
this.setState((state, props) => { this.setState((state, props) => {
return {numUnreadMessages: state.numUnreadMessages + 1}; return {numUnreadMessages: state.numUnreadMessages + 1};
}); });
@ -616,38 +599,8 @@ module.exports = React.createClass({
}, },
_updatePreviewUrlVisibility: function(room) { _updatePreviewUrlVisibility: function(room) {
// console.log("_updatePreviewUrlVisibility");
// check our per-room overrides
const roomPreviewUrls = room.getAccountData("org.matrix.room.preview_urls");
if (roomPreviewUrls && roomPreviewUrls.getContent().disable !== undefined) {
this.setState({ this.setState({
showUrlPreview: !roomPreviewUrls.getContent().disable, showUrlPreview: SettingsStore.getValue("urlPreviewsEnabled", room.roomId),
});
return;
}
// check our global disable override
const userRoomPreviewUrls = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls");
if (userRoomPreviewUrls && userRoomPreviewUrls.getContent().disable) {
this.setState({
showUrlPreview: false,
});
return;
}
// check the room state event
const roomStatePreviewUrls = room.currentState.getStateEvents('org.matrix.room.preview_urls', '');
if (roomStatePreviewUrls && roomStatePreviewUrls.getContent().disable) {
this.setState({
showUrlPreview: false,
});
return;
}
// otherwise, we assume they're on.
this.setState({
showUrlPreview: true,
}); });
}, },
@ -666,12 +619,7 @@ module.exports = React.createClass({
const room = this.state.room; const room = this.state.room;
if (!room) return; if (!room) return;
const color_scheme_event = room.getAccountData("org.matrix.room.color_scheme"); const color_scheme = SettingsStore.getValue("roomColor", room.room_id);
let color_scheme = {};
if (color_scheme_event) {
color_scheme = color_scheme_event.getContent();
// XXX: we should validate the event
}
console.log("Tinter.tint from updateTint"); console.log("Tinter.tint from updateTint");
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
}, },
@ -750,39 +698,10 @@ module.exports = React.createClass({
return; return;
} }
const joinedMembers = room.currentState.getMembers().filter(m => m.membership === "join" || m.membership === "invite"); const joinedMembers = room.currentState.getMembers().filter((m) => m.membership === "join" || m.membership === "invite");
this.setState({isAlone: joinedMembers.length === 1}); this.setState({isAlone: joinedMembers.length === 1});
}, },
_getUnsentMessageError: function(room) {
const unsentMessages = this._getUnsentMessages(room);
if (!unsentMessages.length) return "";
if (
unsentMessages.length === 1 &&
unsentMessages[0].error &&
unsentMessages[0].error.data &&
unsentMessages[0].error.data.error &&
unsentMessages[0].error.name !== "UnknownDeviceError"
) {
return unsentMessages[0].error.data.error;
}
for (const event of unsentMessages) {
if (!event.error || event.error.name !== "UnknownDeviceError") {
return _t("Some of your messages have not been sent.");
}
}
return _t("Message not sent due to unknown devices being present");
},
_getUnsentMessages: function(room) {
if (!room) { return []; }
return room.getPendingEvents().filter(function(ev) {
return ev.status === Matrix.EventStatus.NOT_SENT;
});
},
_updateConfCallNotification: function() { _updateConfCallNotification: function() {
const room = this.state.room; const room = this.state.room;
if (!room || !this.props.ConferenceHandler) { if (!room || !this.props.ConferenceHandler) {
@ -827,14 +746,6 @@ module.exports = React.createClass({
} }
}, },
onResendAllClick: function() {
Resend.resendUnsentEvents(this.state.room);
},
onCancelAllClick: function() {
Resend.cancelUnsentEvents(this.state.room);
},
onInviteButtonClick: function() { onInviteButtonClick: function() {
// call AddressPickerDialog // call AddressPickerDialog
dis.dispatch({ dis.dispatch({
@ -943,9 +854,13 @@ module.exports = React.createClass({
ev.dataTransfer.dropEffect = 'none'; ev.dataTransfer.dropEffect = 'none';
const items = ev.dataTransfer.items; const items = [...ev.dataTransfer.items];
if (items.length == 1) { if (items.length >= 1) {
if (items[0].kind == 'file') { const isDraggingFiles = items.every(function(item) {
return item.kind == 'file';
});
if (isDraggingFiles) {
this.setState({ draggingFile: true }); this.setState({ draggingFile: true });
ev.dataTransfer.dropEffect = 'copy'; ev.dataTransfer.dropEffect = 'copy';
} }
@ -956,10 +871,8 @@ module.exports = React.createClass({
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
this.setState({ draggingFile: false }); this.setState({ draggingFile: false });
const files = ev.dataTransfer.files; const files = [...ev.dataTransfer.files];
if (files.length == 1) { files.forEach(this.uploadFile);
this.uploadFile(files[0]);
}
}, },
onDragLeaveOrEnd: function(ev) { onDragLeaveOrEnd: function(ev) {
@ -978,11 +891,7 @@ module.exports = React.createClass({
file, this.state.room.roomId, MatrixClientPeg.get(), file, this.state.room.roomId, MatrixClientPeg.get(),
).done(undefined, (error) => { ).done(undefined, (error) => {
if (error.name === "UnknownDeviceError") { if (error.name === "UnknownDeviceError") {
dis.dispatch({ // Let the staus bar handle this
action: 'unknown_device_error',
err: error,
room: this.state.room,
});
return; return;
} }
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -1614,12 +1523,9 @@ module.exports = React.createClass({
statusBar = <RoomStatusBar statusBar = <RoomStatusBar
room={this.state.room} room={this.state.room}
numUnreadMessages={this.state.numUnreadMessages} numUnreadMessages={this.state.numUnreadMessages}
unsentMessageError={this.state.unsentMessageError}
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline} atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
sentMessageAndIsAlone={this.state.isAlone} sentMessageAndIsAlone={this.state.isAlone}
hasActiveCall={inCall} hasActiveCall={inCall}
onResendAllClick={this.onResendAllClick}
onCancelAllClick={this.onCancelAllClick}
onInviteClick={this.onInviteButtonClick} onInviteClick={this.onInviteButtonClick}
onStopWarningClick={this.onStopAloneWarningClick} onStopWarningClick={this.onStopAloneWarningClick}
onScrollToBottomClick={this.jumpToLiveTimeline} onScrollToBottomClick={this.jumpToLiveTimeline}
@ -1775,7 +1681,7 @@ module.exports = React.createClass({
const messagePanel = ( const messagePanel = (
<TimelinePanel ref={this._gatherTimelinePanelRef} <TimelinePanel ref={this._gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()} timelineSet={this.state.room.getUnfilteredTimelineSet()}
showReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)} showReadReceipts={!SettingsStore.getValue('hideReadReceipts')}
manageReadReceipts={!this.state.isPeeking} manageReadReceipts={!this.state.isPeeking}
manageReadMarkers={!this.state.isPeeking} manageReadMarkers={!this.state.isPeeking}
hidden={hideMessagePanel} hidden={hideMessagePanel}

View file

@ -18,7 +18,7 @@ const React = require("react");
const ReactDOM = require("react-dom"); const ReactDOM = require("react-dom");
const GeminiScrollbar = require('react-gemini-scrollbar'); const GeminiScrollbar = require('react-gemini-scrollbar');
import Promise from 'bluebird'; import Promise from 'bluebird';
const KeyCode = require('../../KeyCode'); import { KeyCode } from '../../Keyboard';
const DEBUG_SCROLL = false; const DEBUG_SCROLL = false;
// var DEBUG_SCROLL = true; // var DEBUG_SCROLL = true;

View file

@ -0,0 +1,116 @@
/*
Copyright 2017 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import FilterStore from '../../stores/FilterStore';
import TagOrderStore from '../../stores/TagOrderStore';
import GroupActions from '../../actions/GroupActions';
import TagOrderActions from '../../actions/TagOrderActions';
import sdk from '../../index';
import dis from '../../dispatcher';
const TagPanel = React.createClass({
displayName: 'TagPanel',
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getInitialState() {
return {
orderedTags: [],
selectedTags: [],
};
},
componentWillMount: function() {
this.unmounted = false;
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
this._filterStoreToken = FilterStore.addListener(() => {
if (this.unmounted) {
return;
}
this.setState({
selectedTags: FilterStore.getSelectedTags(),
});
});
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
if (this.unmounted) {
return;
}
this.setState({
orderedTags: TagOrderStore.getOrderedTags() || [],
});
});
// This could be done by anything with a matrix client
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
},
componentWillUnmount() {
this.unmounted = true;
this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
if (this._filterStoreToken) {
this._filterStoreToken.remove();
}
},
_onGroupMyMembership() {
if (this.unmounted) return;
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
},
onClick() {
dis.dispatch({action: 'deselect_tags'});
},
onCreateGroupClick(ev) {
ev.stopPropagation();
dis.dispatch({action: 'view_create_group'});
},
onTagTileEndDrag() {
dis.dispatch(TagOrderActions.commitTagOrdering(this.context.matrixClient));
},
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
const tags = this.state.orderedTags.map((tag, index) => {
return <DNDTagTile
key={tag + '_' + index}
tag={tag}
selected={this.state.selectedTags.includes(tag)}
onEndDrag={this.onTagTileEndDrag}
/>;
});
return <div className="mx_TagPanel" onClick={this.onClick}>
<div className="mx_TagPanel_tagTileContainer">
{ tags }
</div>
<AccessibleButton className="mx_TagPanel_createGroupButton" onClick={this.onCreateGroupClick}>
<TintableSvg src="img/icons-create-room.svg" width="25" height="25" />
</AccessibleButton>
</div>;
},
});
export default TagPanel;

View file

@ -15,6 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import SettingsStore from "../../settings/SettingsStore";
const React = require('react'); const React = require('react');
const ReactDOM = require("react-dom"); const ReactDOM = require("react-dom");
import Promise from 'bluebird'; import Promise from 'bluebird';
@ -29,8 +31,7 @@ const dis = require("../../dispatcher");
const ObjectUtils = require('../../ObjectUtils'); const ObjectUtils = require('../../ObjectUtils');
const Modal = require("../../Modal"); const Modal = require("../../Modal");
const UserActivity = require("../../UserActivity"); const UserActivity = require("../../UserActivity");
const KeyCode = require('../../KeyCode'); import { KeyCode } from '../../Keyboard';
import UserSettingsStore from '../../UserSettingsStore';
const PAGINATE_SIZE = 20; const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20; const INITIAL_SIZE = 20;
@ -129,8 +130,6 @@ var TimelinePanel = React.createClass({
} }
} }
const syncedSettings = UserSettingsStore.getSyncedSettings();
return { return {
events: [], events: [],
timelineLoading: true, // track whether our room timeline is loading timelineLoading: true, // track whether our room timeline is loading
@ -175,10 +174,10 @@ var TimelinePanel = React.createClass({
clientSyncState: MatrixClientPeg.get().getSyncState(), clientSyncState: MatrixClientPeg.get().getSyncState(),
// should the event tiles have twelve hour times // should the event tiles have twelve hour times
isTwelveHour: syncedSettings.showTwelveHourTimestamps, isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"),
// always show timestamps on event tiles? // always show timestamps on event tiles?
alwaysShowTimestamps: syncedSettings.alwaysShowTimestamps, alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
}; };
}, },

View file

@ -15,6 +15,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
const React = require('react'); const React = require('react');
const ReactDOM = require('react-dom'); const ReactDOM = require('react-dom');
const sdk = require('../../index'); const sdk = require('../../index');
@ -56,133 +58,69 @@ 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>;
}; };
// Enumerate some simple 'flip a bit' UI settings (if any). // Enumerate some simple 'flip a bit' UI settings (if any). The strings provided here
// 'id' gives the key name in the im.vector.web.settings account data event // must be settings defined in SettingsStore.
// 'label' is how we describe it in the UI. const SIMPLE_SETTINGS = [
// Warning: Each "label" string below must be added to i18n/strings/en_EN.json, { id: "urlPreviewsEnabled" },
// since they will be translated when rendered. { id: "autoplayGifsAndVideos" },
const SETTINGS_LABELS = [ { id: "hideReadReceipts" },
{ { id: "dontSendTypingNotifications" },
id: 'autoplayGifsAndVideos', { id: "alwaysShowTimestamps" },
label: _td('Autoplay GIFs and videos'), { id: "showTwelveHourTimestamps" },
}, { id: "hideJoinLeaves" },
{ { id: "hideAvatarChanges" },
id: 'hideReadReceipts', { id: "hideDisplaynameChanges" },
label: _td('Hide read receipts'), { id: "useCompactLayout" },
}, { id: "hideRedactions" },
{ { id: "enableSyntaxHighlightLanguageDetection" },
id: 'dontSendTypingNotifications', { id: "MessageComposerInput.autoReplaceEmoji" },
label: _td("Don't send typing notifications"), { id: "MessageComposerInput.dontSuggestEmoji" },
}, { id: "Pill.shouldHidePillAvatar" },
{ { id: "TextualBody.disableBigEmoji" },
id: 'alwaysShowTimestamps', { id: "VideoView.flipVideoHorizontally" },
label: _td('Always show message timestamps'),
},
{
id: 'showTwelveHourTimestamps',
label: _td('Show timestamps in 12 hour format (e.g. 2:30pm)'),
},
{
id: 'hideJoinLeaves',
label: _td('Hide join/leave messages (invites/kicks/bans unaffected)'),
},
{
id: 'hideAvatarDisplaynameChanges',
label: _td('Hide avatar and display name changes'),
},
{
id: 'useCompactLayout',
label: _td('Use compact timeline layout'),
},
{
id: 'hideRedactions',
label: _td('Hide removed messages'),
},
{
id: 'enableSyntaxHighlightLanguageDetection',
label: _td('Enable automatic language detection for syntax highlighting'),
},
{
id: 'MessageComposerInput.autoReplaceEmoji',
label: _td('Automatically replace plain text Emoji'),
},
{
id: 'MessageComposerInput.dontSuggestEmoji',
label: _td('Disable Emoji suggestions while typing'),
},
{
id: 'Pill.shouldHidePillAvatar',
label: _td('Hide avatars in user and room mentions'),
},
{
id: 'TextualBody.disableBigEmoji',
label: _td('Disable big emoji in chat'),
},
{
id: 'VideoView.flipVideoHorizontally',
label: _td('Mirror local video feed'),
},
/*
{
id: 'useFixedWidthFont',
label: 'Use fixed width font',
},
*/
]; ];
const ANALYTICS_SETTINGS_LABELS = [ // These settings must be defined in SettingsStore
const ANALYTICS_SETTINGS = [
{ {
id: 'analyticsOptOut', id: 'analyticsOptOut',
label: _td('Opt out of analytics'),
fn: function(checked) { fn: function(checked) {
Analytics[checked ? 'disable' : 'enable'](); Analytics[checked ? 'disable' : 'enable']();
}, },
}, },
]; ];
const WEBRTC_SETTINGS_LABELS = [ // These settings must be defined in SettingsStore
const WEBRTC_SETTINGS = [
{ {
id: 'webRtcForceTURN', id: 'webRtcForceTURN',
label: _td('Disable Peer-to-Peer for 1:1 calls'), fn: (val) => {
MatrixClientPeg.get().setForceTURN(val);
},
}, },
]; ];
// Warning: Each "label" string below must be added to i18n/strings/en_EN.json, // These settings must be defined in SettingsStore
// since they will be translated when rendered. const CRYPTO_SETTINGS = [
const CRYPTO_SETTINGS_LABELS = [
{ {
id: 'blacklistUnverifiedDevices', id: 'blacklistUnverifiedDevices',
label: _td('Never send encrypted messages to unverified devices from this device'),
fn: function(checked) { fn: function(checked) {
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked); MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
}, },
}, },
// XXX: this is here for documentation; the actual setting is managed via RoomSettings
// {
// id: 'blacklistUnverifiedDevicesPerRoom'
// label: 'Never send encrypted messages to unverified devices in this room',
// }
]; ];
// Enumerate the available themes, with a nice human text label. // Enumerate the available themes, with a nice human text label.
// 'id' gives the key name in the im.vector.web.settings account data event
// 'value' is the value for that key in the event
// 'label' is how we describe it in the UI. // 'label' is how we describe it in the UI.
// 'value' is the value for the theme setting
// //
// XXX: Ideally we would have a theme manifest or something and they'd be nicely // XXX: Ideally we would have a theme manifest or something and they'd be nicely
// packaged up in a single directory, and/or located at the application layer. // packaged up in a single directory, and/or located at the application layer.
// But for now for expedience we just hardcode them here. // But for now for expedience we just hardcode them here.
const THEMES = [ const THEMES = [
{ { label: _td('Light theme'), value: 'light' },
id: 'theme', { label: _td('Dark theme'), value: 'dark' },
label: _td('Light theme'), { label: _td('Status.im theme'), value: 'status' },
value: 'light',
},
{
id: 'theme',
label: _td('Dark theme'),
value: 'dark',
},
]; ];
const IgnoredUser = React.createClass({ const IgnoredUser = React.createClass({
@ -204,7 +142,7 @@ const IgnoredUser = React.createClass({
render: function() { render: function() {
return ( return (
<li> <li>
<AccessibleButton onClick={this._onUnignoreClick} className="mx_UserSettings_button mx_UserSettings_buttonSmall"> <AccessibleButton onClick={this._onUnignoreClick} className="mx_textButton">
{ _t("Unignore") } { _t("Unignore") }
</AccessibleButton> </AccessibleButton>
{ this.props.userId } { this.props.userId }
@ -281,14 +219,6 @@ module.exports = React.createClass({
}); });
this._refreshFromServer(); this._refreshFromServer();
const syncedSettings = UserSettingsStore.getSyncedSettings();
if (!syncedSettings.theme) {
syncedSettings.theme = 'light';
}
this._syncedSettings = syncedSettings;
this._localSettings = UserSettingsStore.getLocalSettings();
if (PlatformPeg.get().isElectron()) { if (PlatformPeg.get().isElectron()) {
const {ipcRenderer} = require('electron'); const {ipcRenderer} = require('electron');
@ -359,8 +289,8 @@ module.exports = React.createClass({
if (this._unmounted) return; if (this._unmounted) return;
this.setState({ this.setState({
mediaDevices, mediaDevices,
activeAudioInput: this._localSettings['webrtc_audioinput'], activeAudioInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audioinput'),
activeVideoInput: this._localSettings['webrtc_videoinput'], activeVideoInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_videoinput'),
}); });
}); });
}, },
@ -492,10 +422,6 @@ module.exports = React.createClass({
dis.dispatch({action: 'password_changed'}); dis.dispatch({action: 'password_changed'});
}, },
onEnableNotificationsChange: function(event) {
UserSettingsStore.setEnableNotifications(event.target.checked);
},
_onAddEmailEditFinished: function(value, shouldSubmit) { _onAddEmailEditFinished: function(value, shouldSubmit) {
if (!shouldSubmit) return; if (!shouldSubmit) return;
this._addEmail(); this._addEmail();
@ -669,6 +595,11 @@ module.exports = React.createClass({
}); });
}, },
_renderGroupSettings: function() {
const GroupUserSettings = sdk.getComponent('groups.GroupUserSettings');
return <GroupUserSettings />;
},
_renderReferral: function() { _renderReferral: function() {
const teamToken = this.props.teamToken; const teamToken = this.props.teamToken;
if (!teamToken) { if (!teamToken) {
@ -692,7 +623,7 @@ module.exports = React.createClass({
onLanguageChange: function(newLang) { onLanguageChange: function(newLang) {
if (this.state.language !== newLang) { if (this.state.language !== newLang) {
UserSettingsStore.setLocalSetting('language', newLang); SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
this.setState({ this.setState({
language: newLang, language: newLang,
}); });
@ -715,14 +646,13 @@ module.exports = React.createClass({
// TODO: this ought to be a separate component so that we don't need // TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render // to rebind the onChange each time we render
const onChange = (e) => const onChange = (e) =>
UserSettingsStore.setLocalSetting('autocompleteDelay', + e.target.value); SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
return ( return (
<div> <div>
<h3>{ _t("User Interface") }</h3> <h3>{ _t("User Interface") }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
{ this._renderUrlPreviewSelector() } { SIMPLE_SETTINGS.map( this._renderAccountSetting ) }
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) } { THEMES.map( this._renderThemeOption ) }
{ THEMES.map( this._renderThemeSelector ) }
<table> <table>
<tbody> <tbody>
<tr> <tr>
@ -730,7 +660,7 @@ module.exports = React.createClass({
<td> <td>
<input <input
type="number" type="number"
defaultValue={UserSettingsStore.getLocalSetting('autocompleteDelay', 200)} defaultValue={SettingsStore.getValueAt(SettingLevel.DEVICE, "autocompleteDelay")}
onChange={onChange} onChange={onChange}
/> />
</td> </td>
@ -743,69 +673,31 @@ module.exports = React.createClass({
); );
}, },
_renderUrlPreviewSelector: function() { _renderAccountSetting: function(setting) {
return <div className="mx_UserSettings_toggle"> const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
<input id="urlPreviewsDisabled" return (
type="checkbox" <div className="mx_UserSettings_toggle" key={setting.id}>
defaultChecked={UserSettingsStore.getUrlPreviewsDisabled()} <SettingsFlag name={setting.id}
onChange={this._onPreviewsDisabledChanged} label={setting.label}
/> level={SettingLevel.ACCOUNT}
<label htmlFor="urlPreviewsDisabled"> onChange={setting.fn} />
{ _t("Disable inline URL previews by default") } </div>
</label> );
</div>;
}, },
_onPreviewsDisabledChanged: function(e) { _renderThemeOption: function(setting) {
UserSettingsStore.setUrlPreviewsDisabled(e.target.checked); const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
}, const onChange = (v) => dis.dispatch({action: 'set_theme', value: setting.value});
return (
_renderSyncedSetting: function(setting) { <div className="mx_UserSettings_toggle" key={setting.id + '_' + setting.value}>
// TODO: this ought to be a separate component so that we don't need <SettingsFlag name="theme"
// to rebind the onChange each time we render label={setting.label}
level={SettingLevel.ACCOUNT}
const onChange = (e) => {
UserSettingsStore.setSyncedSetting(setting.id, e.target.checked);
if (setting.fn) setting.fn(e.target.checked);
};
return <div className="mx_UserSettings_toggle" key={setting.id}>
<input id={setting.id}
type="checkbox"
defaultChecked={this._syncedSettings[setting.id]}
onChange={onChange} onChange={onChange}
/> group="theme"
<label htmlFor={setting.id}> value={setting.value} />
{ _t(setting.label) } </div>
</label> );
</div>;
},
_renderThemeSelector: function(setting) {
// TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render
const onChange = (e) => {
if (e.target.checked) {
this._syncedSettings[setting.id] = setting.value;
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
}
dis.dispatch({
action: 'set_theme',
value: setting.value,
});
};
return <div className="mx_UserSettings_toggle" key={setting.id + "_" + setting.value}>
<input id={setting.id + "_" + setting.value}
type="radio"
name={setting.id}
value={setting.value}
checked={this._syncedSettings[setting.id] === setting.value}
onChange={onChange}
/>
<label htmlFor={setting.id + "_" + setting.value}>
{ _t(setting.label) }
</label>
</div>;
}, },
_renderCryptoInfo: function() { _renderCryptoInfo: function() {
@ -847,7 +739,7 @@ module.exports = React.createClass({
{ importExportButtons } { importExportButtons }
</div> </div>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
{ CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) } { CRYPTO_SETTINGS.map( this._renderDeviceSetting ) }
</div> </div>
</div> </div>
); );
@ -873,29 +765,16 @@ module.exports = React.createClass({
} else return (<div />); } else return (<div />);
}, },
_renderLocalSetting: function(setting) { _renderDeviceSetting: function(setting) {
// TODO: this ought to be a separate component so that we don't need const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
// to rebind the onChange each time we render return (
const onChange = (e) => { <div className="mx_UserSettings_toggle" key={setting.id}>
// XXX: awful, but at time of writing, granular settings has landed on <SettingsFlag name={setting.id}
// develop which will almost certainly mean we'll handle this differently. label={setting.label}
if (setting.id === 'webRtcForceTURN') { level={SettingLevel.DEVICE}
MatrixClientPeg.get().setForceTURN(e.target.checked); onChange={setting.fn} />
} </div>
UserSettingsStore.setLocalSetting(setting.id, e.target.checked); );
if (setting.fn) setting.fn(e.target.checked);
};
return <div className="mx_UserSettings_toggle" key={setting.id}>
<input id={setting.id}
type="checkbox"
defaultChecked={this._localSettings[setting.id]}
onChange={onChange}
/>
<label htmlFor={setting.id}>
{ _t(setting.label) }
</label>
</div>;
}, },
_renderDevicesPanel: function() { _renderDevicesPanel: function() {
@ -932,18 +811,18 @@ module.exports = React.createClass({
<h3>{ _t('Analytics') }</h3> <h3>{ _t('Analytics') }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
{ _t('Riot collects anonymous analytics to allow us to improve the application.') } { _t('Riot collects anonymous analytics to allow us to improve the application.') }
{ ANALYTICS_SETTINGS_LABELS.map( this._renderLocalSetting ) } { ANALYTICS_SETTINGS.map( this._renderDeviceSetting ) }
</div> </div>
</div>; </div>;
}, },
_renderLabs: function() { _renderLabs: function() {
const features = []; const features = [];
UserSettingsStore.getLabsFeatures().forEach((featureId) => { SettingsStore.getLabsFeatures().forEach((featureId) => {
// TODO: this ought to be a separate component so that we don't need // TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render // to rebind the onChange each time we render
const onChange = (e) => { const onChange = (e) => {
UserSettingsStore.setFeatureEnabled(featureId, e.target.checked); SettingsStore.setFeatureEnabled(featureId, e.target.checked);
this.forceUpdate(); this.forceUpdate();
}; };
@ -953,10 +832,10 @@ module.exports = React.createClass({
type="checkbox" type="checkbox"
id={featureId} id={featureId}
name={featureId} name={featureId}
defaultChecked={UserSettingsStore.isFeatureEnabled(featureId)} defaultChecked={SettingsStore.isFeatureEnabled(featureId)}
onChange={onChange} onChange={onChange}
/> />
<label htmlFor={featureId}>{ UserSettingsStore.translatedNameForFeature(featureId) }</label> <label htmlFor={featureId}>{ SettingsStore.getDisplayName(featureId) }</label>
</div>); </div>);
}); });
@ -1049,6 +928,8 @@ module.exports = React.createClass({
const settings = this.state.electron_settings; const settings = this.state.electron_settings;
if (!settings) return; if (!settings) return;
// TODO: This should probably be a granular setting, but it only applies to electron
// and ends up being get/set outside of matrix anyways (local system setting).
return <div> return <div>
<h3>{ _t('Desktop specific') }</h3> <h3>{ _t('Desktop specific') }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
@ -1171,7 +1052,7 @@ module.exports = React.createClass({
return <div> return <div>
<h3>{ _t('VoIP') }</h3> <h3>{ _t('VoIP') }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
{ WEBRTC_SETTINGS_LABELS.map(this._renderLocalSetting) } { WEBRTC_SETTINGS.map(this._renderDeviceSetting) }
{ this._renderWebRtcDeviceSettings() } { this._renderWebRtcDeviceSettings() }
</div> </div>
</div>; </div>;
@ -1373,6 +1254,8 @@ module.exports = React.createClass({
{ accountJsx } { accountJsx }
</div> </div>
{ this._renderGroupSettings() }
{ this._renderReferral() } { this._renderReferral() }
{ notificationArea } { notificationArea }

View file

@ -17,13 +17,13 @@ limitations under the License.
'use strict'; 'use strict';
const React = require('react'); import React from 'react';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
const sdk = require('../../../index'); import sdk from '../../../index';
const Modal = require("../../../Modal"); import Modal from "../../../Modal";
const MatrixClientPeg = require('../../../MatrixClientPeg'); import MatrixClientPeg from "../../../MatrixClientPeg";
const PasswordReset = require("../../../PasswordReset"); import PasswordReset from "../../../PasswordReset";
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'ForgotPassword', displayName: 'ForgotPassword',
@ -154,6 +154,7 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
const LoginPage = sdk.getComponent("login.LoginPage");
const LoginHeader = sdk.getComponent("login.LoginHeader"); const LoginHeader = sdk.getComponent("login.LoginHeader");
const LoginFooter = sdk.getComponent("login.LoginFooter"); const LoginFooter = sdk.getComponent("login.LoginFooter");
const ServerConfig = sdk.getComponent("login.ServerConfig"); const ServerConfig = sdk.getComponent("login.ServerConfig");
@ -165,7 +166,7 @@ module.exports = React.createClass({
resetPasswordJsx = <Spinner />; resetPasswordJsx = <Spinner />;
} else if (this.state.progress === "sent_email") { } else if (this.state.progress === "sent_email") {
resetPasswordJsx = ( resetPasswordJsx = (
<div> <div className="mx_Login_prompt">
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", { emailAddress: this.state.email }) } { _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", { emailAddress: this.state.email }) }
<br /> <br />
<input className="mx_Login_submit" type="button" onClick={this.onVerify} <input className="mx_Login_submit" type="button" onClick={this.onVerify}
@ -174,7 +175,7 @@ module.exports = React.createClass({
); );
} else if (this.state.progress === "complete") { } else if (this.state.progress === "complete") {
resetPasswordJsx = ( resetPasswordJsx = (
<div> <div className="mx_Login_prompt">
<p>{ _t('Your password has been reset') }.</p> <p>{ _t('Your password has been reset') }.</p>
<p>{ _t('You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device') }.</p> <p>{ _t('You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device') }.</p>
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete} <input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
@ -182,6 +183,20 @@ module.exports = React.createClass({
</div> </div>
); );
} else { } else {
let serverConfigSection;
if (!config.disable_custom_urls) {
serverConfigSection = (
<ServerConfig ref="serverConfig"
withToggleButton={true}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={0} />
);
}
resetPasswordJsx = ( resetPasswordJsx = (
<div> <div>
<div className="mx_Login_prompt"> <div className="mx_Login_prompt">
@ -209,16 +224,7 @@ module.exports = React.createClass({
<br /> <br />
<input className="mx_Login_submit" type="submit" value={_t('Send Reset Email')} /> <input className="mx_Login_submit" type="submit" value={_t('Send Reset Email')} />
</form> </form>
<ServerConfig ref="serverConfig" { serverConfigSection }
withToggleButton={true}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={0} />
<div className="mx_Login_error">
</div>
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#"> <a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
{ _t('Return to login screen') } { _t('Return to login screen') }
</a> </a>
@ -233,12 +239,12 @@ module.exports = React.createClass({
return ( return (
<div className="mx_Login"> <LoginPage>
<div className="mx_Login_box"> <div className="mx_Login_box">
<LoginHeader /> <LoginHeader />
{ resetPasswordJsx } { resetPasswordJsx }
</div> </div>
</div> </LoginPage>
); );
}, },
}); });

View file

@ -18,12 +18,13 @@ limitations under the License.
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import { _t, _tJsx } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as languageHandler from '../../../languageHandler'; import * as languageHandler from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import Login from '../../../Login'; import Login from '../../../Login';
import UserSettingsStore from '../../../UserSettingsStore';
import PlatformPeg from '../../../PlatformPeg'; import PlatformPeg from '../../../PlatformPeg';
import SdkConfig from '../../../SdkConfig';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
// For validating phone numbers without country codes // For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/; const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
@ -113,7 +114,22 @@ module.exports = React.createClass({
if (error.httpStatus == 400 && usingEmail) { if (error.httpStatus == 400 && usingEmail) {
errorText = _t('This Home Server does not support login using email address.'); errorText = _t('This Home Server does not support login using email address.');
} else if (error.httpStatus === 401 || error.httpStatus === 403) { } else if (error.httpStatus === 401 || error.httpStatus === 403) {
if (SdkConfig.get().disable_custom_urls) {
errorText = (
<div>
<div>{ _t('Incorrect username and/or password.') }</div>
<div className="mx_Login_smallError">
{ _t('Please note you are logging into the %(hs)s server, not matrix.org.',
{
hs: this.props.defaultHsUrl.replace(/^https?:\/\//, ''),
})
}
</div>
</div>
);
} else {
errorText = _t('Incorrect username and/or password.'); errorText = _t('Incorrect username and/or password.');
}
} else { } else {
// other errors, not specific to doing a password login // other errors, not specific to doing a password login
errorText = this._errorTextFromError(error); errorText = this._errorTextFromError(error);
@ -290,17 +306,19 @@ module.exports = React.createClass({
!this.state.enteredHomeserverUrl.startsWith("http")) !this.state.enteredHomeserverUrl.startsWith("http"))
) { ) {
errorText = <span> errorText = <span>
{ _tJsx("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " + {
_t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
"Either use HTTPS or <a>enable unsafe scripts</a>.", "Either use HTTPS or <a>enable unsafe scripts</a>.",
/<a>(.*?)<\/a>/, {},
(sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; }, { 'a': (sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; } },
) } ) }
</span>; </span>;
} else { } else {
errorText = <span> errorText = <span>
{ _tJsx("Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.", {
/<a>(.*?)<\/a>/, _t("Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
(sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; }, {},
{ 'a': (sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; } },
) } ) }
</span>; </span>;
} }
@ -350,7 +368,7 @@ module.exports = React.createClass({
_onLanguageChange: function(newLang) { _onLanguageChange: function(newLang) {
if (languageHandler.getCurrentLanguage() !== newLang) { if (languageHandler.getCurrentLanguage() !== newLang) {
UserSettingsStore.setLocalSetting('language', newLang); SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
PlatformPeg.get().reload(); PlatformPeg.get().reload();
} }
}, },
@ -367,6 +385,7 @@ module.exports = React.createClass({
render: function() { render: function() {
const Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
const LoginPage = sdk.getComponent("login.LoginPage");
const LoginHeader = sdk.getComponent("login.LoginHeader"); const LoginHeader = sdk.getComponent("login.LoginHeader");
const LoginFooter = sdk.getComponent("login.LoginFooter"); const LoginFooter = sdk.getComponent("login.LoginFooter");
const ServerConfig = sdk.getComponent("login.ServerConfig"); const ServerConfig = sdk.getComponent("login.ServerConfig");
@ -381,43 +400,68 @@ module.exports = React.createClass({
} }
let returnToAppJsx; let returnToAppJsx;
/*
// with the advent of ILAG I don't think we need this any more
if (this.props.onCancelClick) { if (this.props.onCancelClick) {
returnToAppJsx = returnToAppJsx =
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#"> <a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
{ _t('Return to app') } { _t('Return to app') }
</a>; </a>;
} }
*/
return ( let serverConfig;
<div className="mx_Login"> let header;
<div className="mx_Login_box">
<LoginHeader /> if (!SdkConfig.get().disable_custom_urls) {
<div> serverConfig = <ServerConfig ref="serverConfig"
<h2>{ _t('Sign in') }
{ loader }
</h2>
{ this.componentForStep(this.state.currentFlow) }
<ServerConfig ref="serverConfig"
withToggleButton={true} withToggleButton={true}
customHsUrl={this.props.customHsUrl} customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl} customIsUrl={this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl} defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl} defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange} onServerConfigChange={this.onServerConfigChange}
delayTimeMs={1000} /> delayTimeMs={1000} />;
}
// FIXME: remove status.im theme tweaks
const theme = SettingsStore.getValue("theme");
if (theme !== "status") {
header = <h2>{ _t('Sign in') }</h2>;
} else {
if (!this.state.errorText) {
header = <h2>{ _t('Sign in to get started') }</h2>;
}
}
let errorTextSection;
if (this.state.errorText) {
errorTextSection = (
<div className="mx_Login_error"> <div className="mx_Login_error">
{ this.state.errorText } { this.state.errorText }
</div> </div>
);
}
return (
<LoginPage>
<div className="mx_Login_box">
<LoginHeader />
<div>
{ header }
{ errorTextSection }
{ this.componentForStep(this.state.currentFlow) }
{ serverConfig }
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#"> <a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
{ _t('Create an account') } { _t('Create an account') }
</a> </a>
{ loginAsGuestJsx } { loginAsGuestJsx }
{ returnToAppJsx } { returnToAppJsx }
{ this._renderLanguageSetting() } { !SdkConfig.get().disable_login_language_selector ? this._renderLanguageSetting() : '' }
<LoginFooter /> <LoginFooter />
</div> </div>
</div> </div>
</div> </LoginPage>
); );
}, },
}); });

View file

@ -59,9 +59,10 @@ module.exports = React.createClass({
render: function() { render: function() {
const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName'); const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
const LoginPage = sdk.getComponent('login.LoginPage');
const LoginHeader = sdk.getComponent('login.LoginHeader'); const LoginHeader = sdk.getComponent('login.LoginHeader');
return ( return (
<div className="mx_Login"> <LoginPage>
<div className="mx_Login_box"> <div className="mx_Login_box">
<LoginHeader /> <LoginHeader />
<div className="mx_Login_profile"> <div className="mx_Login_profile">
@ -74,7 +75,7 @@ module.exports = React.createClass({
{ this.state.errorString } { this.state.errorString }
</div> </div>
</div> </div>
</div> </LoginPage>
); );
}, },
}); });

View file

@ -26,6 +26,8 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import RegistrationForm from '../../views/login/RegistrationForm'; import RegistrationForm from '../../views/login/RegistrationForm';
import RtsClient from '../../../RtsClient'; import RtsClient from '../../../RtsClient';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import SettingsStore from "../../../settings/SettingsStore";
const MIN_PASSWORD_LENGTH = 6; const MIN_PASSWORD_LENGTH = 6;
@ -322,10 +324,13 @@ module.exports = React.createClass({
render: function() { render: function() {
const LoginHeader = sdk.getComponent('login.LoginHeader'); const LoginHeader = sdk.getComponent('login.LoginHeader');
const LoginFooter = sdk.getComponent('login.LoginFooter'); const LoginFooter = sdk.getComponent('login.LoginFooter');
const LoginPage = sdk.getComponent('login.LoginPage');
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth'); const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
const Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
const ServerConfig = sdk.getComponent('views.login.ServerConfig'); const ServerConfig = sdk.getComponent('views.login.ServerConfig');
const theme = SettingsStore.getValue("theme");
let registerBody; let registerBody;
if (this.state.doingUIAuth) { if (this.state.doingUIAuth) {
registerBody = ( registerBody = (
@ -344,9 +349,19 @@ module.exports = React.createClass({
} else if (this.state.busy || this.state.teamServerBusy) { } else if (this.state.busy || this.state.teamServerBusy) {
registerBody = <Spinner />; registerBody = <Spinner />;
} else { } else {
let errorSection; let serverConfigSection;
if (this.state.errorText) { if (!SdkConfig.get().disable_custom_urls) {
errorSection = <div className="mx_Login_error">{ this.state.errorText }</div>; serverConfigSection = (
<ServerConfig ref="serverConfig"
withToggleButton={true}
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={1000}
/>
);
} }
registerBody = ( registerBody = (
<div> <div>
@ -362,21 +377,14 @@ module.exports = React.createClass({
onRegisterClick={this.onFormSubmit} onRegisterClick={this.onFormSubmit}
onTeamSelected={this.onTeamSelected} onTeamSelected={this.onTeamSelected}
/> />
{ errorSection } { serverConfigSection }
<ServerConfig ref="serverConfig"
withToggleButton={true}
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={1000}
/>
</div> </div>
); );
} }
let returnToAppJsx; let returnToAppJsx;
/*
// with the advent of ILAG I don't think we need this any more
if (this.props.onCancelClick) { if (this.props.onCancelClick) {
returnToAppJsx = ( returnToAppJsx = (
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#"> <a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
@ -384,8 +392,31 @@ module.exports = React.createClass({
</a> </a>
); );
} }
*/
let header;
let errorText;
// FIXME: remove hardcoded Status team tweaks at some point
if (theme === 'status' && this.state.errorText) {
header = <div className="mx_Login_error">{ this.state.errorText }</div>;
} else {
header = <h2>{ _t('Create an account') }</h2>;
if (this.state.errorText) {
errorText = <div className="mx_Login_error">{ this.state.errorText }</div>;
}
}
let signIn;
if (!this.state.doingUIAuth) {
signIn = (
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
{ theme === 'status' ? _t('Sign in') : _t('I already have an account') }
</a>
);
}
return ( return (
<div className="mx_Login"> <LoginPage>
<div className="mx_Login_box"> <div className="mx_Login_box">
<LoginHeader <LoginHeader
icon={this.state.teamSelected ? icon={this.state.teamSelected ?
@ -393,15 +424,14 @@ module.exports = React.createClass({
this.state.teamSelected.domain + "/icon.png" : this.state.teamSelected.domain + "/icon.png" :
null} null}
/> />
<h2>{ _t('Create an account') }</h2> { header }
{ registerBody } { registerBody }
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#"> { signIn }
{ _t('I already have an account') } { errorText }
</a>
{ returnToAppJsx } { returnToAppJsx }
<LoginFooter /> <LoginFooter />
</div> </div>
</div> </LoginPage>
); );
}, },
}); });

View file

@ -0,0 +1,168 @@
/*
Copyright 2017 Travis Ralston
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.
*/
'use strict';
import React from "react";
import * as sdk from "../../../index";
import MatrixClientPeg from "../../../MatrixClientPeg";
import AccessibleButton from '../elements/AccessibleButton';
import Presence from "../../../Presence";
import dispatcher from "../../../dispatcher";
import * as ContextualMenu from "../../structures/ContextualMenu";
import SettingsStore from "../../../settings/SettingsStore";
// This is an avatar with presence information and controls on it.
module.exports = React.createClass({
displayName: 'MemberPresenceAvatar',
propTypes: {
member: React.PropTypes.object.isRequired,
width: React.PropTypes.number,
height: React.PropTypes.number,
resizeMethod: React.PropTypes.string,
},
getDefaultProps: function() {
return {
width: 40,
height: 40,
resizeMethod: 'crop',
};
},
getInitialState: function() {
let presenceState = null;
let presenceMessage = null;
// RoomMembers do not necessarily have a user.
if (this.props.member.user) {
presenceState = this.props.member.user.presence;
presenceMessage = this.props.member.user.presenceStatusMsg;
}
return {
status: presenceState,
message: presenceMessage,
};
},
componentWillMount: function() {
MatrixClientPeg.get().on("User.presence", this.onUserPresence);
this.dispatcherRef = dispatcher.register(this.onAction);
},
componentWillUnmount: function() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("User.presence", this.onUserPresence);
}
dispatcher.unregister(this.dispatcherRef);
},
onAction: function(payload) {
if (payload.action !== "self_presence_updated") return;
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) return;
this.setState({
status: payload.statusInfo.presence,
message: payload.statusInfo.status_msg,
});
},
onUserPresence: function(event, user) {
if (user.userId !== MatrixClientPeg.get().getUserId()) return;
this.setState({
status: user.presence,
message: user.presenceStatusMsg,
});
},
onStatusChange: function(newStatus) {
Presence.stopMaintainingStatus();
if (newStatus === "online") {
Presence.setState(newStatus);
} else Presence.setState(newStatus, null, true);
},
onClick: function(e) {
const PresenceContextMenu = sdk.getComponent('context_menus.PresenceContextMenu');
const elementRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const x = (elementRect.left + window.pageXOffset) - (elementRect.width / 2) + 3;
const chevronOffset = 12;
let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron
ContextualMenu.createMenu(PresenceContextMenu, {
chevronOffset: chevronOffset,
chevronFace: 'bottom',
left: x,
top: y,
menuWidth: 125,
currentStatus: this.state.status,
onChange: this.onStatusChange,
});
e.stopPropagation();
// XXX NB the following assumes that user is non-null, which is not valid
// const presenceState = this.props.member.user.presence;
// const presenceLastActiveAgo = this.props.member.user.lastActiveAgo;
// const presenceLastTs = this.props.member.user.lastPresenceTs;
// const presenceCurrentlyActive = this.props.member.user.currentlyActive;
// const presenceMessage = this.props.member.user.presenceStatusMsg;
},
render: function() {
const MemberAvatar = sdk.getComponent("avatars.MemberAvatar");
let onClickFn = null;
if (this.props.member.userId === MatrixClientPeg.get().getUserId()) {
onClickFn = this.onClick;
}
const avatarNode = (
<MemberAvatar member={this.props.member} width={this.props.width} height={this.props.height}
resizeMethod={this.props.resizeMethod} />
);
let statusNode = (
<span className={"mx_MemberPresenceAvatar_status mx_MemberPresenceAvatar_status_" + this.state.status} />
);
// LABS: Disable presence management functions for now
// Also disable the presence information if there's no status information
if (!SettingsStore.isFeatureEnabled("feature_presence_management") || !this.state.status) {
statusNode = null;
onClickFn = null;
}
let avatar = (
<div className="mx_MemberPresenceAvatar">
{ avatarNode }
{ statusNode }
</div>
);
if (onClickFn) {
avatar = (
<AccessibleButton onClick={onClickFn} className="mx_MemberPresenceAvatar" element="div">
{ avatarNode }
{ statusNode }
</AccessibleButton>
);
}
return avatar;
},
});

View file

@ -244,7 +244,7 @@ module.exports = React.createClass({
_doNaiveGroupRoomSearch: function(query) { _doNaiveGroupRoomSearch: function(query) {
const lowerCaseQuery = query.toLowerCase(); const lowerCaseQuery = query.toLowerCase();
const groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), this.props.groupId); const groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
const results = []; const results = [];
groupStore.getGroupRooms().forEach((r) => { groupStore.getGroupRooms().forEach((r) => {
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery); const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import * as KeyCode from '../../../KeyCode'; import { KeyCode } from '../../../Keyboard';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import sdk from '../../../index'; import sdk from '../../../index';

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import classnames from 'classnames';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
/* /*
@ -24,51 +23,17 @@ import { _t } from '../../../languageHandler';
*/ */
export default React.createClass({ export default React.createClass({
displayName: 'ConfirmRedactDialog', displayName: 'ConfirmRedactDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
},
defaultProps: {
danger: false,
},
onOk: function() {
this.props.onFinished(true);
},
onCancel: function() {
this.props.onFinished(false);
},
render: function() { render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
const title = _t("Confirm Removal");
const confirmButtonClass = classnames({
'mx_Dialog_primary': true,
'danger': false,
});
return ( return (
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished} <QuestionDialog onFinished={this.props.onFinished}
onEnterPressed={this.onOk} title={_t("Confirm Removal")}
title={title} description={
> _t("Are you sure you wish to remove (delete) this event? " +
<div className="mx_Dialog_content">
{ _t("Are you sure you wish to remove (delete) this event? " +
"Note that if you delete a room name or topic change, it could undo the change.")} "Note that if you delete a room name or topic change, it could undo the change.")}
</div> button={_t("Remove")}>
<div className="mx_Dialog_buttons"> </QuestionDialog>
<button className={confirmButtonClass} onClick={this.onOk}>
{ _t("Remove") }
</button>
<button onClick={this.onCancel}>
{ _t("Cancel") }
</button>
</div>
</BaseDialog>
); );
}, },
}); });

View file

@ -18,7 +18,7 @@ import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { _t, _tJsx } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
export default React.createClass({ export default React.createClass({
@ -45,9 +45,10 @@ export default React.createClass({
if (SdkConfig.get().bug_report_endpoint_url) { if (SdkConfig.get().bug_report_endpoint_url) {
bugreport = ( bugreport = (
<p> <p>
{ _tJsx( { _t(
"Otherwise, <a>click here</a> to send a bug report.", "Otherwise, <a>click here</a> to send a bug report.",
/<a>(.*?)<\/a>/, (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a>, {},
{ 'a': (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a> },
) } ) }
</p> </p>
); );

View file

@ -20,8 +20,8 @@ import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import classnames from 'classnames'; import classnames from 'classnames';
import KeyCode from '../../../KeyCode'; import { KeyCode } from '../../../Keyboard';
import { _t, _tJsx } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
// The amount of time to wait for further changes to the input username before // The amount of time to wait for further changes to the input username before
// sending a request to the server // sending a request to the server
@ -267,24 +267,21 @@ export default React.createClass({
</div> </div>
{ usernameIndicator } { usernameIndicator }
<p> <p>
{ _tJsx( { _t(
'This will be your account name on the <span></span> ' + 'This will be your account name on the <span></span> ' +
'homeserver, or you can pick a <a>different server</a>.', 'homeserver, or you can pick a <a>different server</a>.',
[ {},
/<span><\/span>/, {
/<a>(.*?)<\/a>/, 'span': <span>{ this.props.homeserverUrl }</span>,
], 'a': (sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
[ },
(sub) => <span>{ this.props.homeserverUrl }</span>,
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
],
) } ) }
</p> </p>
<p> <p>
{ _tJsx( { _t(
'If you already have a Matrix account you can <a>log in</a> instead.', 'If you already have a Matrix account you can <a>log in</a> instead.',
/<a>(.*?)<\/a>/, {},
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a>], { 'a': (sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a> },
) } ) }
</p> </p>
{ auth } { auth }

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2017 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.
@ -15,11 +16,21 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import GeminiScrollbar from 'react-gemini-scrollbar'; import GeminiScrollbar from 'react-gemini-scrollbar';
import Resend from '../../../Resend'; import Resend from '../../../Resend';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
function markAllDevicesKnown(devices) {
Object.keys(devices).forEach((userId) => {
Object.keys(devices[userId]).map((deviceId) => {
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
});
});
}
function DeviceListEntry(props) { function DeviceListEntry(props) {
const {userId, device} = props; const {userId, device} = props;
@ -37,10 +48,10 @@ function DeviceListEntry(props) {
} }
DeviceListEntry.propTypes = { DeviceListEntry.propTypes = {
userId: React.PropTypes.string.isRequired, userId: PropTypes.string.isRequired,
// deviceinfo // deviceinfo
device: React.PropTypes.object.isRequired, device: PropTypes.object.isRequired,
}; };
@ -60,10 +71,10 @@ function UserUnknownDeviceList(props) {
} }
UserUnknownDeviceList.propTypes = { UserUnknownDeviceList.propTypes = {
userId: React.PropTypes.string.isRequired, userId: PropTypes.string.isRequired,
// map from deviceid -> deviceinfo // map from deviceid -> deviceinfo
userDevices: React.PropTypes.object.isRequired, userDevices: PropTypes.object.isRequired,
}; };
@ -82,7 +93,7 @@ function UnknownDeviceList(props) {
UnknownDeviceList.propTypes = { UnknownDeviceList.propTypes = {
// map from userid -> deviceid -> deviceinfo // map from userid -> deviceid -> deviceinfo
devices: React.PropTypes.object.isRequired, devices: PropTypes.object.isRequired,
}; };
@ -90,34 +101,65 @@ export default React.createClass({
displayName: 'UnknownDeviceDialog', displayName: 'UnknownDeviceDialog',
propTypes: { propTypes: {
room: React.PropTypes.object.isRequired, room: PropTypes.object.isRequired,
// map from userid -> deviceid -> deviceinfo // map from userid -> deviceid -> deviceinfo or null if devices are not yet loaded
devices: React.PropTypes.object.isRequired, devices: PropTypes.object,
onFinished: React.PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
// Label for the button that marks all devices known and tries the send again
sendAnywayLabel: PropTypes.string.isRequired,
// Label for the button that to send the event if you've verified all devices
sendLabel: PropTypes.string.isRequired,
// function to retry the request once all devices are verified / known
onSend: PropTypes.func.isRequired,
}, },
componentDidMount: function() { componentWillMount: function() {
// Given we've now shown the user the unknown device, it is no longer MatrixClientPeg.get().on("deviceVerificationChanged", this._onDeviceVerificationChanged);
// unknown to them. Therefore mark it as 'known'. },
Object.keys(this.props.devices).forEach((userId) => {
Object.keys(this.props.devices[userId]).map((deviceId) => {
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
});
});
// XXX: temporary logging to try to diagnose componentWillUnmount: function() {
// https://github.com/vector-im/riot-web/issues/3148 if (MatrixClientPeg.get()) {
console.log('Opening UnknownDeviceDialog'); MatrixClientPeg.get().removeListener("deviceVerificationChanged", this._onDeviceVerificationChanged);
}
},
_onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) {
if (this.props.devices[userId] && this.props.devices[userId][deviceId]) {
// XXX: Mutating props :/
this.props.devices[userId][deviceId] = deviceInfo;
this.forceUpdate();
}
},
_onDismissClicked: function() {
this.props.onFinished();
},
_onSendAnywayClicked: function() {
markAllDevicesKnown(this.props.devices);
this.props.onFinished();
this.props.onSend();
},
_onSendClicked: function() {
this.props.onFinished();
this.props.onSend();
}, },
render: function() { render: function() {
const client = MatrixClientPeg.get(); if (this.props.devices === null) {
const blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() || const Spinner = sdk.getComponent("elements.Spinner");
this.props.room.getBlacklistUnverifiedDevices(); return <Spinner />;
}
let warning; let warning;
if (blacklistUnverified) { if (SettingsStore.getValue("blacklistUnverifiedDevices", this.props.room.roomId)) {
warning = ( warning = (
<h4> <h4>
{ _t("You are currently blacklisting unverified devices; to send " + { _t("You are currently blacklisting unverified devices; to send " +
@ -136,15 +178,30 @@ export default React.createClass({
); );
} }
let haveUnknownDevices = false;
Object.keys(this.props.devices).forEach((userId) => {
Object.keys(this.props.devices[userId]).map((deviceId) => {
const device = this.props.devices[userId][deviceId];
if (device.isUnverified() && !device.isKnown()) {
haveUnknownDevices = true;
}
});
});
let sendButton;
if (haveUnknownDevices) {
sendButton = <button onClick={this._onSendAnywayClicked}>
{ this.props.sendAnywayLabel }
</button>;
} else {
sendButton = <button onClick={this._onSendClicked}>
{ this.props.sendLabel }
</button>;
}
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return ( return (
<BaseDialog className='mx_UnknownDeviceDialog' <BaseDialog className='mx_UnknownDeviceDialog'
onFinished={() => { onFinished={this.props.onFinished}
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
console.log("UnknownDeviceDialog closed by escape");
this.props.onFinished();
}}
title={_t('Room contains unknown devices')} title={_t('Room contains unknown devices')}
> >
<GeminiScrollbar autoshow={false} className="mx_Dialog_content"> <GeminiScrollbar autoshow={false} className="mx_Dialog_content">
@ -157,21 +214,11 @@ export default React.createClass({
<UnknownDeviceList devices={this.props.devices} /> <UnknownDeviceList devices={this.props.devices} />
</GeminiScrollbar> </GeminiScrollbar>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
{sendButton}
<button className="mx_Dialog_primary" autoFocus={true} <button className="mx_Dialog_primary" autoFocus={true}
onClick={() => { onClick={this._onDismissClicked}
this.props.onFinished(); >
Resend.resendUnsentEvents(this.props.room); {_t("Dismiss")}
}}>
{ _t("Send anyway") }
</button>
<button className="mx_Dialog_primary" autoFocus={true}
onClick={() => {
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
console.log("UnknownDeviceDialog closed by OK");
this.props.onFinished();
}}>
OK
</button> </button>
</div> </div>
</BaseDialog> </BaseDialog>

View file

@ -77,6 +77,7 @@ export default React.createClass({
onClick={this._onClick} onClick={this._onClick}
onMouseEnter={this._onMouseEnter} onMouseEnter={this._onMouseEnter}
onMouseLeave={this._onMouseLeave} onMouseLeave={this._onMouseLeave}
aria-label={this.props.label}
> >
<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} /> <TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />
{ tooltip } { tooltip }

View file

@ -22,6 +22,8 @@ import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg'; import PlatformPeg from '../../../PlatformPeg';
import ScalarAuthClient from '../../../ScalarAuthClient'; import ScalarAuthClient from '../../../ScalarAuthClient';
import WidgetMessaging from '../../../WidgetMessaging';
import TintableSvgButton from './TintableSvgButton';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
@ -50,11 +52,13 @@ export default React.createClass({
userId: React.PropTypes.string.isRequired, userId: React.PropTypes.string.isRequired,
// UserId of the entity that added / modified the widget // UserId of the entity that added / modified the widget
creatorUserId: React.PropTypes.string, creatorUserId: React.PropTypes.string,
waitForIframeLoad: React.PropTypes.bool,
}, },
getDefaultProps() { getDefaultProps() {
return { return {
url: "", url: "",
waitForIframeLoad: true,
}; };
}, },
@ -69,17 +73,46 @@ export default React.createClass({
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId); const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
return { return {
initialising: true, // True while we are mangling the widget URL initialising: true, // True while we are mangling the widget URL
loading: true, // True while the iframe content is loading loading: this.props.waitForIframeLoad, // True while the iframe content is loading
widgetUrl: newProps.url, widgetUrl: this._addWurlParams(newProps.url),
widgetPermissionId: widgetPermissionId, widgetPermissionId: widgetPermissionId,
// Assume that widget has permission to load if we are the user who // Assume that widget has permission to load if we are the user who
// added it to the room, or if explicitly granted by the user // added it to the room, or if explicitly granted by the user
hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId, hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId,
error: null, error: null,
deleting: false, deleting: false,
widgetPageTitle: newProps.widgetPageTitle,
}; };
}, },
/**
* Add widget instance specific parameters to pass in wUrl
* Properties passed to widget instance:
* - widgetId
* - origin / parent URL
* @param {string} urlString Url string to modify
* @return {string}
* Url string with parameters appended.
* If url can not be parsed, it is returned unmodified.
*/
_addWurlParams(urlString) {
const u = url.parse(urlString);
if (!u) {
console.error("_addWurlParams", "Invalid URL", urlString);
return url;
}
const params = qs.parse(u.query);
// Append widget ID to query parameters
params.widgetId = this.props.id;
// Append current / parent URL
params.parentUrl = window.location.href;
u.search = undefined;
u.query = params;
return u.format();
},
getInitialState() { getInitialState() {
return this._getNewState(this.props); return this._getNewState(this.props);
}, },
@ -121,6 +154,8 @@ export default React.createClass({
}, },
componentWillMount() { componentWillMount() {
WidgetMessaging.startListening();
WidgetMessaging.addEndpoint(this.props.id, this.props.url);
window.addEventListener('message', this._onMessage, false); window.addEventListener('message', this._onMessage, false);
this.setScalarToken(); this.setScalarToken();
}, },
@ -136,7 +171,7 @@ export default React.createClass({
console.warn('Non-scalar widget, not setting scalar token!', url); console.warn('Non-scalar widget, not setting scalar token!', url);
this.setState({ this.setState({
error: null, error: null,
widgetUrl: this.props.url, widgetUrl: this._addWurlParams(this.props.url),
initialising: false, initialising: false,
}); });
return; return;
@ -149,7 +184,7 @@ export default React.createClass({
this._scalarClient.getScalarToken().done((token) => { this._scalarClient.getScalarToken().done((token) => {
// Append scalar_token as a query param if not already present // Append scalar_token as a query param if not already present
this._scalarClient.scalarToken = token; this._scalarClient.scalarToken = token;
const u = url.parse(this.props.url); const u = url.parse(this._addWurlParams(this.props.url));
const params = qs.parse(u.query); const params = qs.parse(u.query);
if (!params.scalar_token) { if (!params.scalar_token) {
params.scalar_token = encodeURIComponent(token); params.scalar_token = encodeURIComponent(token);
@ -163,6 +198,11 @@ export default React.createClass({
widgetUrl: u.format(), widgetUrl: u.format(),
initialising: false, initialising: false,
}); });
// Fetch page title from remote content if not already set
if (!this.state.widgetPageTitle && params.url) {
this._fetchWidgetTitle(params.url);
}
}, (err) => { }, (err) => {
console.error("Failed to get scalar_token", err); console.error("Failed to get scalar_token", err);
this.setState({ this.setState({
@ -173,6 +213,8 @@ export default React.createClass({
}, },
componentWillUnmount() { componentWillUnmount() {
WidgetMessaging.stopListening();
WidgetMessaging.removeEndpoint(this.props.id, this.props.url);
window.removeEventListener('message', this._onMessage); window.removeEventListener('message', this._onMessage);
}, },
@ -180,10 +222,14 @@ export default React.createClass({
if (nextProps.url !== this.props.url) { if (nextProps.url !== this.props.url) {
this._getNewState(nextProps); this._getNewState(nextProps);
this.setScalarToken(); this.setScalarToken();
} else if (nextProps.show && !this.props.show) { } else if (nextProps.show && !this.props.show && this.props.waitForIframeLoad) {
this.setState({ this.setState({
loading: true, loading: true,
}); });
} else if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) {
this.setState({
widgetPageTitle: nextProps.widgetPageTitle,
});
} }
}, },
@ -255,10 +301,27 @@ export default React.createClass({
} }
}, },
/**
* Called when widget iframe has finished loading
*/
_onLoaded() { _onLoaded() {
this.setState({loading: false}); this.setState({loading: false});
}, },
/**
* Set remote content title on AppTile
* @param {string} url Url to check for title
*/
_fetchWidgetTitle(url) {
this._scalarClient.getScalarPageTitle(url).then((widgetPageTitle) => {
if (widgetPageTitle) {
this.setState({widgetPageTitle: widgetPageTitle});
}
}, (err) =>{
console.error("Failed to get page title", err);
});
},
// Widget labels to render, depending upon user permissions // Widget labels to render, depending upon user permissions
// These strings are translated at the point that they are inserted in to the DOM, in the render method // These strings are translated at the point that they are inserted in to the DOM, in the render method
_deleteWidgetLabel() { _deleteWidgetLabel() {
@ -304,6 +367,15 @@ export default React.createClass({
}); });
}, },
_getSafeUrl() {
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
let safeWidgetUrl = '';
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
safeWidgetUrl = url.format(parsedWidgetUrl);
}
return safeWidgetUrl;
},
render() { render() {
let appTileBody; let appTileBody;
@ -319,11 +391,6 @@ export default React.createClass({
// a link to it. // a link to it.
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+ const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
"allow-same-origin allow-scripts allow-presentation"; "allow-same-origin allow-scripts allow-presentation";
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
let safeWidgetUrl = '';
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
safeWidgetUrl = url.format(parsedWidgetUrl);
}
if (this.props.show) { if (this.props.show) {
const loadingElement = ( const loadingElement = (
@ -346,7 +413,7 @@ export default React.createClass({
{ this.state.loading && loadingElement } { this.state.loading && loadingElement }
<iframe <iframe
ref="appFrame" ref="appFrame"
src={safeWidgetUrl} src={this._getSafeUrl()}
allowFullScreen="true" allowFullScreen="true"
sandbox={sandboxFlags} sandbox={sandboxFlags}
onLoad={this._onLoaded} onLoad={this._onLoaded}
@ -371,35 +438,50 @@ export default React.createClass({
// editing is done in scalar // editing is done in scalar
const showEditButton = Boolean(this._scalarClient && this._canUserModify()); const showEditButton = Boolean(this._scalarClient && this._canUserModify());
const deleteWidgetLabel = this._deleteWidgetLabel(); const deleteWidgetLabel = this._deleteWidgetLabel();
let deleteIcon = 'img/cancel.svg'; let deleteIcon = 'img/cancel_green.svg';
let deleteClasses = 'mx_filterFlipColor mx_AppTileMenuBarWidget'; let deleteClasses = 'mx_AppTileMenuBarWidget';
if (this._canUserModify()) { if (this._canUserModify()) {
deleteIcon = 'img/icon-delete-pink.svg'; deleteIcon = 'img/icon-delete-pink.svg';
deleteClasses += ' mx_AppTileMenuBarWidgetDelete'; deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
} }
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
return ( return (
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}> <div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}> <div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
{ this.formatAppTileName() } <span className="mx_AppTileMenuBarTitle">
<TintableSvgButton
src={windowStateIcon}
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
title={_t('Minimize apps')}
width="10"
height="10"
/>
<b>{ this.formatAppTileName() }</b>
{ this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName() && (
<span>&nbsp;-&nbsp;{ this.state.widgetPageTitle }</span>
) }
</span>
<span className="mx_AppTileMenuBarWidgets"> <span className="mx_AppTileMenuBarWidgets">
{ /* Edit widget */ } { /* Edit widget */ }
{ showEditButton && <img { showEditButton && <TintableSvgButton
src="img/edit.svg" src="img/edit_green.svg"
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding" className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
width="8" height="8"
alt={_t('Edit')}
title={_t('Edit')} title={_t('Edit')}
onClick={this._onEditClick} onClick={this._onEditClick}
width="10"
height="10"
/> } /> }
{ /* Delete widget */ } { /* Delete widget */ }
<img src={deleteIcon} <TintableSvgButton
src={deleteIcon}
className={deleteClasses} className={deleteClasses}
width="8" height="8"
alt={_t(deleteWidgetLabel)}
title={_t(deleteWidgetLabel)} title={_t(deleteWidgetLabel)}
onClick={this._onDeleteClick} onClick={this._onDeleteClick}
width="10"
height="10"
/> />
</span> </span>
</div> </div>

View file

@ -0,0 +1,85 @@
/* eslint new-cap: "off" */
/*
Copyright 2017 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 { DragSource, DropTarget } from 'react-dnd';
import TagTile from './TagTile';
import dis from '../../../dispatcher';
import { findDOMNode } from 'react-dom';
const tagTileSource = {
canDrag: function(props, monitor) {
return true;
},
beginDrag: function(props) {
// Return the data describing the dragged item
return {
tag: props.tag,
};
},
endDrag: function(props, monitor, component) {
const dropResult = monitor.getDropResult();
if (!monitor.didDrop() || !dropResult) {
return;
}
props.onEndDrag();
},
};
const tagTileTarget = {
canDrop(props, monitor) {
return true;
},
hover(props, monitor, component) {
if (!monitor.canDrop()) return;
const draggedY = monitor.getClientOffset().y;
const {top, bottom} = findDOMNode(component).getBoundingClientRect();
const targetY = (top + bottom) / 2;
dis.dispatch({
action: 'order_tag',
tag: monitor.getItem().tag,
targetTag: props.tag,
// Note: we indicate that the tag should be after the target when
// it's being dragged over the top half of the target.
after: draggedY < targetY,
});
},
drop(props) {
// Return the data to be returned by getDropResult
return {
tag: props.tag,
};
},
};
export default
DropTarget('TagTile', tagTileTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
}))(DragSource('TagTile', tagTileSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
}))((props) => {
const { connectDropTarget, connectDragSource, ...otherProps } = props;
return connectDropTarget(connectDragSource(
<div>
<TagTile {...otherProps} />
</div>,
));
}));

View file

@ -72,24 +72,19 @@ export default class Flair extends React.Component {
this.state = { this.state = {
profiles: [], profiles: [],
}; };
this.onRoomStateEvents = this.onRoomStateEvents.bind(this);
} }
componentWillUnmount() { componentWillUnmount() {
this._unmounted = true; this._unmounted = true;
this.context.matrixClient.removeListener('RoomState.events', this.onRoomStateEvents);
} }
componentWillMount() { componentWillMount() {
this._unmounted = false; this._unmounted = false;
this._generateAvatars(); this._generateAvatars(this.props.groups);
this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents);
} }
onRoomStateEvents(event) { componentWillReceiveProps(newProps) {
if (event.getType() === 'm.room.related_groups' && FlairStore.groupSupport()) { this._generateAvatars(newProps.groups);
this._generateAvatars();
}
} }
async _getGroupProfiles(groups) { async _getGroupProfiles(groups) {
@ -106,23 +101,7 @@ export default class Flair extends React.Component {
return profiles.filter((p) => p !== null); return profiles.filter((p) => p !== null);
} }
async _generateAvatars() { async _generateAvatars(groups) {
let groups = await FlairStore.getPublicisedGroupsCached(this.context.matrixClient, this.props.userId);
if (this.props.roomId && this.props.showRelated) {
const relatedGroupsEvent = this.context.matrixClient
.getRoom(this.props.roomId)
.currentState
.getStateEvents('m.room.related_groups', '');
const relatedGroups = relatedGroupsEvent ?
relatedGroupsEvent.getContent().groups || [] : [];
if (relatedGroups && relatedGroups.length > 0) {
groups = groups.filter((groupId) => {
return relatedGroups.includes(groupId);
});
} else {
groups = [];
}
}
if (!groups || groups.length === 0) { if (!groups || groups.length === 0) {
return; return;
} }
@ -148,13 +127,7 @@ export default class Flair extends React.Component {
} }
Flair.propTypes = { Flair.propTypes = {
userId: PropTypes.string, groups: PropTypes.arrayOf(PropTypes.string),
// Whether to show only the flair associated with related groups of the given room,
// or all flair associated with a user.
showRelated: PropTypes.bool,
// The room that this flair will be displayed in. Optional. Only applies when showRelated = true.
roomId: PropTypes.string,
}; };
// TODO: We've decided that all components should follow this pattern, which means removing withMatrixClient and using // TODO: We've decided that all components should follow this pattern, which means removing withMatrixClient and using

View file

@ -18,8 +18,8 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import UserSettingsStore from '../../../UserSettingsStore';
import * as languageHandler from '../../../languageHandler'; import * as languageHandler from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
function languageMatchesSearchQuery(query, language) { function languageMatchesSearchQuery(query, language) {
if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true; if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
@ -54,9 +54,9 @@ export default class LanguageDropdown extends React.Component {
// If no value is given, we start with the first // If no value is given, we start with the first
// country selected, but our parent component // country selected, but our parent component
// doesn't know this, therefore we do this. // doesn't know this, therefore we do this.
const _localSettings = UserSettingsStore.getLocalSettings(); const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
if (_localSettings.hasOwnProperty('language')) { if (language) {
this.props.onOptionChange(_localSettings.language); this.props.onOptionChange(language);
} else { } else {
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser()); const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
this.props.onOptionChange(language); this.props.onOptionChange(language);
@ -95,12 +95,12 @@ export default class LanguageDropdown extends React.Component {
// default value here too, otherwise we need to handle null / undefined // default value here too, otherwise we need to handle null / undefined
// values between mounting and the initial value propgating // values between mounting and the initial value propgating
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
let value = null; let value = null;
const _localSettings = UserSettingsStore.getLocalSettings(); if (language) {
if (_localSettings.hasOwnProperty('language')) { value = this.props.value || language;
value = this.props.value || _localSettings.language;
} else { } else {
const language = navigator.language || navigator.userLanguage; language = navigator.language || navigator.userLanguage;
value = this.props.value || language; value = this.props.value || language;
} }

View file

@ -304,7 +304,7 @@ module.exports = React.createClass({
return items[0]; return items[0];
} else if (remaining > 0) { } else if (remaining > 0) {
items = items.slice(0, itemLimit); items = items.slice(0, itemLimit);
return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } ) return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } );
} else { } else {
const lastItem = items.pop(); const lastItem = items.pop();
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem }); return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });
@ -478,7 +478,7 @@ module.exports = React.createClass({
} }
const toggleButton = ( const toggleButton = (
<div className={"mx_MemberEventListSummary_toggle"} onClick={this._toggleSummary}> <div className={"mx_MemberEventListSummary_toggle"} onClick={this._toggleSummary}>
{ expanded ? 'collapse' : 'expand' } { expanded ? _t('collapse') : _t('expand') }
</div> </div>
); );

View file

@ -20,14 +20,16 @@ import React from 'react';
import * as Roles from '../../../Roles'; import * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
let LEVEL_ROLE_MAP = {};
const reverseRoles = {};
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'PowerSelector', displayName: 'PowerSelector',
propTypes: { propTypes: {
value: React.PropTypes.number.isRequired, value: React.PropTypes.number.isRequired,
// The maximum value that can be set with the power selector
maxValue: React.PropTypes.number.isRequired,
// Default user power level for the room
usersDefault: React.PropTypes.number.isRequired,
// if true, the <select/> should be a 'controlled' form element and updated by React // if true, the <select/> should be a 'controlled' form element and updated by React
// to reflect the current value, rather than left freeform. // to reflect the current value, rather than left freeform.
@ -43,78 +45,98 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
custom: (LEVEL_ROLE_MAP[this.props.value] === undefined), levelRoleMap: {},
// List of power levels to show in the drop-down
options: [],
};
},
getDefaultProps: function() {
return {
maxValue: Infinity,
usersDefault: 0,
}; };
}, },
componentWillMount: function() { componentWillMount: function() {
LEVEL_ROLE_MAP = Roles.levelRoleMap(); this._initStateFromProps(this.props);
Object.keys(LEVEL_ROLE_MAP).forEach(function(key) { },
reverseRoles[LEVEL_ROLE_MAP[key]] = key;
componentWillReceiveProps: function(newProps) {
this._initStateFromProps(newProps);
},
_initStateFromProps: function(newProps) {
// This needs to be done now because levelRoleMap has translated strings
const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault);
const options = Object.keys(levelRoleMap).filter((l) => {
return l === undefined || l <= newProps.maxValue;
});
this.setState({
levelRoleMap,
options,
custom: levelRoleMap[newProps.value] === undefined,
}); });
}, },
onSelectChange: function(event) { onSelectChange: function(event) {
this.setState({ custom: event.target.value === "Custom" }); this.setState({ custom: event.target.value === "SELECT_VALUE_CUSTOM" });
if (event.target.value !== "Custom") { if (event.target.value !== "SELECT_VALUE_CUSTOM") {
this.props.onChange(this.getValue()); this.props.onChange(event.target.value);
} }
}, },
onCustomBlur: function(event) { onCustomBlur: function(event) {
this.props.onChange(this.getValue()); this.props.onChange(parseInt(this.refs.custom.value));
}, },
onCustomKeyDown: function(event) { onCustomKeyDown: function(event) {
if (event.key == "Enter") { if (event.key == "Enter") {
this.props.onChange(this.getValue()); this.props.onChange(parseInt(this.refs.custom.value));
} }
}, },
getValue: function() {
let value;
if (this.refs.select) {
value = reverseRoles[this.refs.select.value];
if (this.refs.custom) {
if (value === undefined) value = parseInt( this.refs.custom.value );
}
}
return value;
},
render: function() { render: function() {
let customPicker; let customPicker;
if (this.state.custom) { if (this.state.custom) {
let input;
if (this.props.disabled) { if (this.props.disabled) {
input = <span>{ this.props.value }</span>; customPicker = <span>{ _t(
"Custom of %(powerLevel)s",
{ powerLevel: this.props.value },
) }</span>;
} else { } else {
input = <input ref="custom" type="text" size="3" defaultValue={this.props.value} onBlur={this.onCustomBlur} onKeyDown={this.onCustomKeyDown} />; customPicker = <span> = <input
ref="custom"
type="text"
size="3"
defaultValue={this.props.value}
onBlur={this.onCustomBlur}
onKeyDown={this.onCustomKeyDown}
/>
</span>;
} }
customPicker = <span> of { input }</span>;
} }
let selectValue; let selectValue;
if (this.state.custom) { if (this.state.custom) {
selectValue = "Custom"; selectValue = "SELECT_VALUE_CUSTOM";
} else { } else {
selectValue = LEVEL_ROLE_MAP[this.props.value] || "Custom"; selectValue = this.state.levelRoleMap[this.props.value] ?
this.props.value : "SELECT_VALUE_CUSTOM";
} }
let select; let select;
if (this.props.disabled) { if (this.props.disabled) {
select = <span>{ selectValue }</span>; select = <span>{ this.state.levelRoleMap[selectValue] }</span>;
} else { } else {
// Each level must have a definition in LEVEL_ROLE_MAP // Each level must have a definition in this.state.levelRoleMap
const levels = [0, 50, 100]; let options = this.state.options.map((level) => {
let options = levels.map((level) => {
return { return {
value: LEVEL_ROLE_MAP[level], value: level,
// Give a userDefault (users_default in the power event) of 0 but text: Roles.textualPowerLevel(level, this.props.usersDefault),
// because level !== undefined, this should never be used.
text: Roles.textualPowerLevel(level, 0),
}; };
}); });
options.push({ value: "Custom", text: _t("Custom level") }); options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") });
options = options.map((op) => { options = options.map((op) => {
return <option value={op.value} key={op.value}>{ op.text }</option>; return <option value={op.value} key={op.value}>{ op.text }</option>;
}); });

View file

@ -0,0 +1,110 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'SettingsFlag',
propTypes: {
name: React.PropTypes.string.isRequired,
level: React.PropTypes.string.isRequired,
roomId: React.PropTypes.string, // for per-room settings
label: React.PropTypes.string, // untranslated
onChange: React.PropTypes.func,
isExplicit: React.PropTypes.bool,
manualSave: React.PropTypes.bool,
// If group is supplied, then this will create a radio button instead.
group: React.PropTypes.string,
value: React.PropTypes.any, // the value for the radio button
},
getInitialState: function() {
return {
value: SettingsStore.getValueAt(
this.props.level,
this.props.name,
this.props.roomId,
this.props.isExplicit,
),
};
},
onChange: function(e) {
if (this.props.group && !e.target.checked) return;
const newState = this.props.group ? this.props.value : e.target.checked;
if (!this.props.manualSave) this.save(newState);
else this.setState({ value: newState });
if (this.props.onChange) this.props.onChange(newState);
},
save: function(val = undefined) {
return SettingsStore.setValue(
this.props.name,
this.props.roomId,
this.props.level,
val !== undefined ? val : this.state.value,
);
},
render: function() {
const value = this.props.manualSave ? this.state.value : SettingsStore.getValueAt(
this.props.level,
this.props.name,
this.props.roomId,
this.props.isExplicit,
);
const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level);
let label = this.props.label;
if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level);
else label = _t(label);
// We generate a relatively complex ID to avoid conflicts
const id = this.props.name + "_" + this.props.group + "_" + this.props.value + "_" + this.props.level;
let checkbox = (
<input id={id}
type="checkbox"
defaultChecked={value}
onChange={this.onChange}
disabled={!canChange}
/>
);
if (this.props.group) {
checkbox = (
<input id={id}
type="radio"
name={this.props.group}
value={this.props.value}
checked={value === this.props.value}
onChange={this.onChange}
disabled={!canChange}
/>
);
}
return (
<label>
{ checkbox }
{ label }
</label>
);
},
});

View file

@ -0,0 +1,119 @@
/*
Copyright 2017 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
import FlairStore from '../../../stores/FlairStore';
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
// a thing to click on for the user to filter the visible rooms in the RoomList to:
// - Rooms that are part of the group
// - Direct messages with members of the group
// with the intention that this could be expanded to arbitrary tags in future.
export default React.createClass({
displayName: 'TagTile',
propTypes: {
// A string tag such as "m.favourite" or a group ID such as "+groupid:domain.bla"
// For now, only group IDs are handled.
tag: PropTypes.string,
},
contextTypes: {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
},
getInitialState() {
return {
// Whether the mouse is over the tile
hover: false,
// The profile data of the group if this.props.tag is a group ID
profile: null,
};
},
componentWillMount() {
this.unmounted = false;
if (this.props.tag[0] === '+') {
FlairStore.getGroupProfileCached(
this.context.matrixClient,
this.props.tag,
).then((profile) => {
if (this.unmounted) return;
this.setState({profile});
}).catch((err) => {
console.warn('Could not fetch group profile for ' + this.props.tag, err);
});
}
},
componentWillUnmount() {
this.unmounted = true;
},
onClick: function(e) {
e.preventDefault();
e.stopPropagation();
dis.dispatch({
action: 'select_tag',
tag: this.props.tag,
ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e),
shiftKey: e.shiftKey,
});
},
onMouseOver: function() {
this.setState({hover: true});
},
onMouseOut: function() {
this.setState({hover: false});
},
render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
const profile = this.state.profile || {};
const name = profile.name || this.props.tag;
const avatarHeight = 35;
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
) : null;
const className = classNames({
mx_TagTile: true,
mx_TagTile_selected: this.props.selected,
});
const tip = this.state.hover ?
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
<div />;
return <AccessibleButton className={className} onClick={this.onClick}>
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
{ tip }
</div>
</AccessibleButton>;
},
});

View file

@ -0,0 +1,61 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import TintableSvg from './TintableSvg';
export default class TintableSvgButton extends React.Component {
constructor(props) {
super(props);
}
render() {
let classes = "mx_TintableSvgButton";
if (this.props.className) {
classes += " " + this.props.className;
}
return (
<span
width={this.props.width}
height={this.props.height}
className={classes}>
<TintableSvg
src={this.props.src}
width={this.props.width}
height={this.props.height}
></TintableSvg>
<span
title={this.props.title}
onClick={this.props.onClick} />
</span>
);
}
}
TintableSvgButton.propTypes = {
src: PropTypes.string,
title: PropTypes.string,
className: PropTypes.string,
width: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
onClick: PropTypes.func,
};
TintableSvgButton.defaultProps = {
onClick: function() {},
};

View file

@ -59,9 +59,7 @@ module.exports = React.createClass({
}, },
_initGroupStore(groupId) { _initGroupStore(groupId) {
this._groupStore = GroupStoreCache.getGroupStore( this._groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
this.context.matrixClient, this.props.groupId,
);
this._groupStore.registerListener(this.onGroupStoreUpdated); this._groupStore.registerListener(this.onGroupStoreUpdated);
}, },

View file

@ -20,17 +20,12 @@ import sdk from '../../../index';
import GroupStoreCache from '../../../stores/GroupStoreCache'; import GroupStoreCache from '../../../stores/GroupStoreCache';
import GeminiScrollbar from 'react-gemini-scrollbar'; import GeminiScrollbar from 'react-gemini-scrollbar';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import withMatrixClient from '../../../wrappers/withMatrixClient';
const INITIAL_LOAD_NUM_MEMBERS = 30; const INITIAL_LOAD_NUM_MEMBERS = 30;
export default withMatrixClient(React.createClass({ export default React.createClass({
displayName: 'GroupMemberList', displayName: 'GroupMemberList',
contextTypes: {
matrixClient: PropTypes.object.isRequired,
},
propTypes: { propTypes: {
groupId: PropTypes.string.isRequired, groupId: PropTypes.string.isRequired,
}, },
@ -49,7 +44,7 @@ export default withMatrixClient(React.createClass({
}, },
_initGroupStore: function(groupId) { _initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId); this._groupStore = GroupStoreCache.getGroupStore(groupId);
this._groupStore.registerListener(() => { this._groupStore.registerListener(() => {
this._fetchMembers(); this._fetchMembers();
}); });
@ -92,7 +87,7 @@ export default withMatrixClient(React.createClass({
query = (query || "").toLowerCase(); query = (query || "").toLowerCase();
if (query) { if (query) {
memberList = memberList.filter((m) => { memberList = memberList.filter((m) => {
const matchesName = m.displayname.toLowerCase().indexOf(query) !== -1; const matchesName = (m.displayname || "").toLowerCase().includes(query);
const matchesId = m.userId.toLowerCase().includes(query); const matchesId = m.userId.toLowerCase().includes(query);
if (!matchesName && !matchesId) { if (!matchesName && !matchesId) {
@ -174,4 +169,4 @@ export default withMatrixClient(React.createClass({
</div> </div>
); );
}, },
})); });

View file

@ -0,0 +1,86 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GroupStore from '../../../stores/GroupStore';
import { _t } from '../../../languageHandler.js';
export default React.createClass({
displayName: 'GroupPublicityToggle',
propTypes: {
groupId: PropTypes.string.isRequired,
},
getInitialState() {
return {
busy: false,
ready: false,
isGroupPublicised: null,
};
},
componentWillMount: function() {
this._initGroupStore(this.props.groupId);
},
_initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(groupId);
this._groupStore.registerListener(() => {
this.setState({
isGroupPublicised: this._groupStore.getGroupPublicity(),
ready: this._groupStore.isStateReady(GroupStore.STATE_KEY.Summary),
});
});
},
_onPublicityToggle: function(e) {
e.stopPropagation();
this.setState({
busy: true,
// Optimistic early update
isGroupPublicised: !this.state.isGroupPublicised,
});
this._groupStore.setGroupPublicity(!this.state.isGroupPublicised).then(() => {
this.setState({
busy: false,
});
});
},
render() {
const GroupTile = sdk.getComponent('groups.GroupTile');
const input = <input type="checkbox"
onClick={this._onPublicityToggle}
checked={this.state.isGroupPublicised}
/>;
const labelText = !this.state.ready ? _t("Loading...") :
(this.state.isGroupPublicised ?
_t("Flair will appear if enabled in room settings") :
_t("Flair will not appear")
);
return <div className="mx_GroupPublicity_toggle">
<GroupTile groupId={this.props.groupId} showDescription={false} avatarHeight={40} />
<label onClick={this._onPublicityToggle}>
{ input }
{ labelText }
</label>
</div>;
},
});

View file

@ -61,9 +61,7 @@ module.exports = React.createClass({
}, },
_initGroupStore(groupId) { _initGroupStore(groupId) {
this._groupStore = GroupStoreCache.getGroupStore( this._groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
this.context.matrixClient, this.props.groupId,
);
this._groupStore.registerListener(this.onGroupStoreUpdated); this._groupStore.registerListener(this.onGroupStoreUpdated);
}, },

View file

@ -19,15 +19,10 @@ import sdk from '../../../index';
import GroupStoreCache from '../../../stores/GroupStoreCache'; import GroupStoreCache from '../../../stores/GroupStoreCache';
import GeminiScrollbar from 'react-gemini-scrollbar'; import GeminiScrollbar from 'react-gemini-scrollbar';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {MatrixClient} from 'matrix-js-sdk';
const INITIAL_LOAD_NUM_ROOMS = 30; const INITIAL_LOAD_NUM_ROOMS = 30;
export default React.createClass({ export default React.createClass({
contextTypes: {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
},
propTypes: { propTypes: {
groupId: PropTypes.string.isRequired, groupId: PropTypes.string.isRequired,
}, },
@ -46,7 +41,7 @@ export default React.createClass({
}, },
_initGroupStore: function(groupId) { _initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId); this._groupStore = GroupStoreCache.getGroupStore(groupId);
this._groupStore.registerListener(() => { this._groupStore.registerListener(() => {
this._fetchRooms(); this._fetchRooms();
}); });

View file

@ -0,0 +1,93 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClient} from 'matrix-js-sdk';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import FlairStore from '../../../stores/FlairStore';
const GroupTile = React.createClass({
displayName: 'GroupTile',
propTypes: {
groupId: PropTypes.string.isRequired,
// Whether to show the short description of the group on the tile
showDescription: PropTypes.bool,
// Height of the group avatar in pixels
avatarHeight: PropTypes.number,
},
contextTypes: {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
},
getInitialState() {
return {
profile: null,
};
},
getDefaultProps() {
return {
showDescription: true,
avatarHeight: 50,
};
},
componentWillMount: function() {
FlairStore.getGroupProfileCached(this.context.matrixClient, this.props.groupId).then((profile) => {
this.setState({profile});
}).catch((err) => {
console.error('Error whilst getting cached profile for GroupTile', err);
});
},
onClick: function(e) {
e.preventDefault();
dis.dispatch({
action: 'view_group',
group_id: this.props.groupId,
});
},
render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const profile = this.state.profile || {};
const name = profile.name || this.props.groupId;
const avatarHeight = this.props.avatarHeight;
const descElement = this.props.showDescription ?
<div className="mx_GroupTile_desc">{ profile.shortDescription }</div> :
<div />;
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
) : null;
return <AccessibleButton className="mx_GroupTile" onClick={this.onClick}>
<div className="mx_GroupTile_avatar">
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
</div>
<div className="mx_GroupTile_profile">
<div className="mx_GroupTile_name">{ name }</div>
{ descElement }
<div className="mx_GroupTile_groupId">{ this.props.groupId }</div>
</div>
</AccessibleButton>;
},
});
export default GroupTile;

View file

@ -0,0 +1,89 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import GeminiScrollbar from 'react-gemini-scrollbar';
import sdk from '../../../index';
import { MatrixClient } from 'matrix-js-sdk';
import { _t } from '../../../languageHandler';
export default React.createClass({
displayName: 'GroupUserSettings',
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getInitialState() {
return {
error: null,
groups: null,
};
},
componentWillMount: function() {
this.context.matrixClient.getJoinedGroups().done((result) => {
this.setState({groups: result.groups || [], error: null});
}, (err) => {
console.error(err);
this.setState({groups: null, error: err});
});
},
_renderGroupPublicity() {
let text = "";
let scrollbox = <div />;
const groups = this.state.groups;
if (this.state.error) {
text = _t('Something went wrong when trying to get your communities.');
} else if (groups === null) {
text = _t('Loading...');
} else if (groups.length > 0) {
const GroupPublicityToggle = sdk.getComponent('groups.GroupPublicityToggle');
const groupPublicityToggles = groups.map((groupId, index) => {
return <GroupPublicityToggle key={index} groupId={groupId} />;
});
text = _t('Display your community flair in rooms configured to show it.');
scrollbox = <div className="mx_GroupUserSettings_groupPublicity_scrollbox">
<GeminiScrollbar>
{ groupPublicityToggles }
</GeminiScrollbar>
</div>;
} else {
text = _t("You're not currently a member of any communities.");
}
return <div>
<h3>{ _t('Flair') }</h3>
<div className="mx_UserSettings_section">
<p>
{ text }
</p>
{ scrollbox }
</div>
</div>;
},
render() {
const groupPublicity = this._renderGroupPublicity();
return <div>
{ groupPublicity }
</div>;
},
});

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { _t, _tJsx } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
const DIV_ID = 'mx_recaptcha'; const DIV_ID = 'mx_recaptcha';
@ -67,10 +67,10 @@ module.exports = React.createClass({
// * jumping straight to a hosted captcha page (but we don't support that yet) // * jumping straight to a hosted captcha page (but we don't support that yet)
// * embedding the captcha in an iframe (if that works) // * embedding the captcha in an iframe (if that works)
// * using a better captcha lib // * using a better captcha lib
ReactDOM.render(_tJsx( ReactDOM.render(_t(
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>", "Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
/<a>(.*?)<\/a>/, {},
(sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }), warning); { 'a': (sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }}), warning);
this.refs.recaptchaContainer.appendChild(warning); this.refs.recaptchaContainer.appendChild(warning);
} else { } else {
const scriptTag = document.createElement('script'); const scriptTag = document.createElement('script');

View file

@ -20,7 +20,7 @@ import url from 'url';
import classnames from 'classnames'; import classnames from 'classnames';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t, _tJsx } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
/* This file contains a collection of components which are used by the /* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed * InteractiveAuth to prompt the user to enter the information needed
@ -256,7 +256,10 @@ export const EmailIdentityAuthEntry = React.createClass({
} else { } else {
return ( return (
<div> <div>
<p>{ _tJsx("An email has been sent to %(emailAddress)s", /%\(emailAddress\)s/, (sub) => <i>{this.props.inputs.emailAddress}</i>) }</p> <p>{ _t("An email has been sent to %(emailAddress)s",
{ emailAddress: (sub) => <i>{ this.props.inputs.emailAddress }</i> },
) }
</p>
<p>{ _t("Please check your email to continue registration.") }</p> <p>{ _t("Please check your email to continue registration.") }</p>
</div> </div>
); );
@ -370,7 +373,10 @@ export const MsisdnAuthEntry = React.createClass({
}); });
return ( return (
<div> <div>
<p>{ _tJsx("A text message has been sent to %(msisdn)s", /%\(msisdn\)s/, (sub) => <i>{this._msisdn}</i>) }</p> <p>{ _t("A text message has been sent to %(msisdn)s",
{ msisdn: <i>{ this._msisdn }</i> },
) }
</p>
<p>{ _t("Please enter the code it contains:") }</p> <p>{ _t("Please enter the code it contains:") }</p>
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper"> <div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
<form onSubmit={this._onFormSubmit}> <form onSubmit={this._onFormSubmit}>

View file

@ -0,0 +1,59 @@
/*
Copyright 2015, 2016 OpenMarket 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.
*/
'use strict';
import SettingsStore from "../../../settings/SettingsStore";
const React = require('react');
module.exports = React.createClass({
displayName: 'LoginPage',
render: function() {
// FIXME: this should be turned into a proper skin with a StatusLoginPage component
if (SettingsStore.getValue("theme") === 'status') {
return (
<div className="mx_StatusLogin">
<div className="mx_StatusLogin_brand">
<img src="themes/status/img/logo.svg" alt="Status" width="221" height="53" />
</div>
<div className="mx_StatusLogin_content">
<div className="mx_StatusLogin_header">
<h1>Status Community Chat</h1>
<div className="mx_StatusLogin_subtitle">
A safer, decentralised communication
platform <a href="https://riot.im">powered by Riot</a>
</div>
</div>
{ this.props.children }
<div className="mx_StatusLogin_footer">
<p>This channel is for our development community.</p>
<p>Interested in SNT and discussions on the cryptocurrency market?</p>
<p><a href="https://t.me/StatusNetworkChat" target="_blank" className="mx_StatusLogin_footer_cta">Join Telegram Chat</a></p>
</div>
</div>
</div>
);
} else {
return (
<div className="mx_Login">
{ this.props.children }
</div>
);
}
},
});

View file

@ -20,7 +20,7 @@ import classNames from 'classnames';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {field_input_incorrect} from '../../../UiEffects'; import {field_input_incorrect} from '../../../UiEffects';
import SdkConfig from '../../../SdkConfig';
/** /**
* A pure UI component which displays a username/password form. * A pure UI component which displays a username/password form.
@ -144,7 +144,10 @@ class PasswordLogin extends React.Component {
type="text" type="text"
name="username" // make it a little easier for browser's remember-password name="username" // make it a little easier for browser's remember-password
onChange={this.onUsernameChanged} onChange={this.onUsernameChanged}
placeholder={_t('User name')} placeholder={SdkConfig.get().disable_custom_urls ?
_t("Username on %(hs)s", {
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
}) : _t("User name")}
value={this.state.username} value={this.state.username}
autoFocus autoFocus
disabled={disabled} disabled={disabled}
@ -210,9 +213,9 @@ class PasswordLogin extends React.Component {
const loginField = this.renderLoginField(this.state.loginType, matrixIdText === ''); const loginField = this.renderLoginField(this.state.loginType, matrixIdText === '');
return ( let loginType;
<div> if (!SdkConfig.get().disable_3pid_login) {
<form onSubmit={this.onSubmitForm}> loginType = (
<div className="mx_Login_type_container"> <div className="mx_Login_type_container">
<label className="mx_Login_type_label">{ _t('Sign in with') }</label> <label className="mx_Login_type_label">{ _t('Sign in with') }</label>
<Dropdown <Dropdown
@ -225,6 +228,13 @@ class PasswordLogin extends React.Component {
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span> <span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span>
</Dropdown> </Dropdown>
</div> </div>
);
}
return (
<div>
<form onSubmit={this.onSubmitForm}>
{ loginType }
{ loginField } { loginField }
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password" <input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
name="password" name="password"

View file

@ -22,6 +22,8 @@ import Email from '../../../email';
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import SettingsStore from "../../../settings/SettingsStore";
const FIELD_EMAIL = 'field_email'; const FIELD_EMAIL = 'field_email';
const FIELD_PHONE_COUNTRY = 'field_phone_country'; const FIELD_PHONE_COUNTRY = 'field_phone_country';
@ -122,7 +124,7 @@ module.exports = React.createClass({
password: this.refs.password.value.trim(), password: this.refs.password.value.trim(),
email: email, email: email,
phoneCountry: this.state.phoneCountry, phoneCountry: this.state.phoneCountry,
phoneNumber: this.refs.phoneNumber.value.trim(), phoneNumber: this.refs.phoneNumber ? this.refs.phoneNumber.value.trim() : '',
}); });
if (promise) { if (promise) {
@ -180,7 +182,7 @@ module.exports = React.createClass({
this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID"); this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
break; break;
case FIELD_PHONE_NUMBER: case FIELD_PHONE_NUMBER:
const phoneNumber = this.refs.phoneNumber.value; const phoneNumber = this.refs.phoneNumber ? this.refs.phoneNumber.value : '';
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber); const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID"); this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
break; break;
@ -273,10 +275,14 @@ module.exports = React.createClass({
render: function() { render: function() {
const self = this; const self = this;
const theme = SettingsStore.getValue("theme");
// FIXME: remove hardcoded Status team tweaks at some point
const emailPlaceholder = theme === 'status' ? _t("Email address") : _t("Email address (optional)");
const emailSection = ( const emailSection = (
<div> <div>
<input type="text" ref="email" <input type="text" ref="email"
autoFocus={true} placeholder={_t("Email address (optional)")} autoFocus={true} placeholder={emailPlaceholder}
defaultValue={this.props.defaultEmail} defaultValue={this.props.defaultEmail}
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')} className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_EMAIL);}} onBlur={function() {self.validateField(FIELD_EMAIL);}}
@ -306,7 +312,9 @@ module.exports = React.createClass({
} }
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
const phoneSection = ( let phoneSection;
if (!SdkConfig.get().disable_3pid_login) {
phoneSection = (
<div className="mx_Login_phoneSection"> <div className="mx_Login_phoneSection">
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange} <CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
className="mx_Login_phoneCountry mx_Login_field_prefix" className="mx_Login_phoneCountry mx_Login_field_prefix"
@ -328,6 +336,7 @@ module.exports = React.createClass({
/> />
</div> </div>
); );
}
const registerButton = ( const registerButton = (
<input className="mx_Login_submit" type="submit" value={_t("Register")} /> <input className="mx_Login_submit" type="submit" value={_t("Register")} />

View file

@ -25,8 +25,8 @@ import sdk from '../../../index';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile'; import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import Promise from 'bluebird'; import Promise from 'bluebird';
import UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MImageBody', displayName: 'MImageBody',
@ -81,7 +81,7 @@ module.exports = React.createClass({
}, },
onImageEnter: function(e) { onImageEnter: function(e) {
if (!this._isGif() || UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) { if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
return; return;
} }
const imgElement = e.target; const imgElement = e.target;
@ -89,7 +89,7 @@ module.exports = React.createClass({
}, },
onImageLeave: function(e) { onImageLeave: function(e) {
if (!this._isGif() || UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) { if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
return; return;
} }
const imgElement = e.target; const imgElement = e.target;
@ -218,7 +218,7 @@ module.exports = React.createClass({
const contentUrl = this._getContentUrl(); const contentUrl = this._getContentUrl();
let thumbUrl; let thumbUrl;
if (this._isGif() && UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) { if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
thumbUrl = contentUrl; thumbUrl = contentUrl;
} else { } else {
thumbUrl = this._getThumbUrl(); thumbUrl = this._getThumbUrl();
@ -230,6 +230,7 @@ module.exports = React.createClass({
<a href={contentUrl} onClick={this.onClick}> <a href={contentUrl} onClick={this.onClick}>
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image" <img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
alt={content.body} alt={content.body}
onLoad={this.props.onWidgetLoad}
onMouseEnter={this.onImageEnter} onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} /> onMouseLeave={this.onImageLeave} />
</a> </a>

View file

@ -21,8 +21,8 @@ import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile'; import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import Promise from 'bluebird'; import Promise from 'bluebird';
import UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MVideoBody', displayName: 'MVideoBody',
@ -151,7 +151,7 @@ module.exports = React.createClass({
const contentUrl = this._getContentUrl(); const contentUrl = this._getContentUrl();
const thumbUrl = this._getThumbUrl(); const thumbUrl = this._getThumbUrl();
const autoplay = UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false); const autoplay = SettingsStore.getValue("autoplayGifsAndVideos");
let height = null; let height = null;
let width = null; let width = null;
let poster = null; let poster = null;

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import { ContentRepo } from 'matrix-js-sdk'; import { ContentRepo } from 'matrix-js-sdk';
import { _t, _tJsx } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
@ -67,24 +67,17 @@ module.exports = React.createClass({
'crop', 'crop',
); );
// it sucks that _tJsx doesn't support normal _t substitutions :((
return ( return (
<div className="mx_RoomAvatarEvent"> <div className="mx_RoomAvatarEvent">
{ _tJsx('%(senderDisplayName)s changed the room avatar to <img/>', { _t('%(senderDisplayName)s changed the room avatar to <img/>',
[ { senderDisplayName: senderDisplayName },
/%\(senderDisplayName\)s/, {
/<img\/>/, 'img': () =>
],
[
(sub) => senderDisplayName,
(sub) =>
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar" <AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
onClick={this.onAvatarClick.bind(this, name)}> onClick={this.onAvatarClick.bind(this, name)}>
<BaseAvatar width={14} height={14} url={url} <BaseAvatar width={14} height={14} url={url} name={name} />
name={name} />
</AccessibleButton>, </AccessibleButton>,
], })
)
} }
</div> </div>
); );

View file

@ -17,53 +17,128 @@
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClient} from 'matrix-js-sdk';
import sdk from '../../../index'; import sdk from '../../../index';
import Flair from '../elements/Flair.js'; import Flair from '../elements/Flair.js';
import { _tJsx } from '../../../languageHandler'; import FlairStore from '../../../stores/FlairStore';
import { _t } from '../../../languageHandler';
export default function SenderProfile(props) { export default React.createClass({
displayName: 'SenderProfile',
propTypes: {
mxEvent: PropTypes.object.isRequired, // event whose sender we're showing
text: PropTypes.string, // Text to show. Defaults to sender name
onClick: PropTypes.func,
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getInitialState() {
return {
userGroups: null,
relatedGroups: [],
};
},
componentWillMount() {
this.unmounted = false;
this._updateRelatedGroups();
FlairStore.getPublicisedGroupsCached(
this.context.matrixClient, this.props.mxEvent.getSender(),
).then((userGroups) => {
if (this.unmounted) return;
this.setState({userGroups});
});
this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents);
},
componentWillUnmount() {
this.unmounted = true;
this.context.matrixClient.removeListener('RoomState.events', this.onRoomStateEvents);
},
onRoomStateEvents(event) {
if (event.getType() === 'm.room.related_groups' &&
event.getRoomId() === this.props.mxEvent.getRoomId()
) {
this._updateRelatedGroups();
}
},
_updateRelatedGroups() {
if (this.unmounted) return;
const relatedGroupsEvent = this.context.matrixClient
.getRoom(this.props.mxEvent.getRoomId())
.currentState
.getStateEvents('m.room.related_groups', '');
this.setState({
relatedGroups: relatedGroupsEvent ?
relatedGroupsEvent.getContent().groups || []
: [],
});
},
_getDisplayedGroups(userGroups, relatedGroups) {
let displayedGroups = userGroups || [];
if (relatedGroups && relatedGroups.length > 0) {
displayedGroups = displayedGroups.filter((groupId) => {
return relatedGroups.includes(groupId);
});
} else {
displayedGroups = [];
}
return displayedGroups;
},
render() {
const EmojiText = sdk.getComponent('elements.EmojiText'); const EmojiText = sdk.getComponent('elements.EmojiText');
const {mxEvent} = props; const {mxEvent} = this.props;
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); let name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
const {msgtype} = mxEvent.getContent(); const {msgtype} = mxEvent.getContent();
if (msgtype === 'm.emote') { if (msgtype === 'm.emote') {
return <span />; // emote message must include the name so don't duplicate it return <span />; // emote message must include the name so don't duplicate it
} }
// Name + flair let flair = <div />;
const nameElem = [ if (this.props.enableFlair) {
<EmojiText key='name' className="mx_SenderProfile_name">{ name || '' }</EmojiText>, const displayedGroups = this._getDisplayedGroups(
props.enableFlair ? this.state.userGroups, this.state.relatedGroups,
<Flair key='flair' );
// Backwards-compatible replacing of "(IRC)" with AS user flair
name = displayedGroups.length > 0 ? name.replace(' (IRC)', '') : name;
flair = <Flair key='flair'
userId={mxEvent.getSender()} userId={mxEvent.getSender()}
roomId={mxEvent.getRoomId()} groups={displayedGroups}
showRelated={true} /> />;
: null,
];
let content = '';
if(props.text) {
// Replace senderName, and wrap surrounding text in spans with the right class
content = _tJsx(props.text, /^(.*)\%\(senderName\)s(.*)$/m, (p1, p2) => [
p1 ? <span className='mx_SenderProfile_aux'>{ p1 }</span> : null,
nameElem,
p2 ? <span className='mx_SenderProfile_aux'>{ p2 }</span> : null,
]);
} else {
content = nameElem;
} }
const nameElem = <EmojiText key='name'>{ name || '' }</EmojiText>;
// Name + flair
const nameFlair = <span>
<span className="mx_SenderProfile_name">
{ nameElem }
</span>
{ flair }
</span>;
const content = this.props.text ?
<span className="mx_SenderProfile_aux">
{ _t(this.props.text, { senderName: () => nameElem }) }
</span> : nameFlair;
return ( return (
<div className="mx_SenderProfile" dir="auto" onClick={props.onClick}> <div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
{ content } { content }
</div> </div>
); );
} },
});
SenderProfile.propTypes = {
mxEvent: React.PropTypes.object.isRequired, // event whose sender we're showing
text: React.PropTypes.string, // Text to show. Defaults to sender name
onClick: React.PropTypes.func,
};

View file

@ -29,11 +29,11 @@ import Modal from '../../../Modal';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import UserSettingsStore from "../../../UserSettingsStore";
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import ContextualMenu from '../../structures/ContextualMenu'; import ContextualMenu from '../../structures/ContextualMenu';
import {RoomMember} from 'matrix-js-sdk'; import {RoomMember} from 'matrix-js-sdk';
import classNames from 'classnames'; import classNames from 'classnames';
import SettingsStore from "../../../settings/SettingsStore";
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor'; import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
linkifyMatrix(linkify); linkifyMatrix(linkify);
@ -104,7 +104,7 @@ module.exports = React.createClass({
setTimeout(() => { setTimeout(() => {
if (this._unmounted) return; if (this._unmounted) return;
for (let i = 0; i < blocks.length; i++) { for (let i = 0; i < blocks.length; i++) {
if (UserSettingsStore.getSyncedSetting("enableSyntaxHighlightLanguageDetection", false)) { if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
highlight.highlightBlock(blocks[i]); highlight.highlightBlock(blocks[i]);
} else { } else {
// Only syntax highlight if there's a class starting with language- // Only syntax highlight if there's a class starting with language-
@ -169,7 +169,7 @@ module.exports = React.createClass({
}, },
pillifyLinks: function(nodes) { pillifyLinks: function(nodes) {
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false); const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
let node = nodes[0]; let node = nodes[0];
while (node) { while (node) {
let pillified = false; let pillified = false;
@ -419,7 +419,7 @@ module.exports = React.createClass({
const content = mxEvent.getContent(); const content = mxEvent.getContent();
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, { let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
disableBigEmoji: UserSettingsStore.getSyncedSetting('TextualBody.disableBigEmoji', false), disableBigEmoji: SettingsStore.getValue('TextualBody.disableBigEmoji'),
}); });
if (this.props.highlightLink) { if (this.props.highlightLink) {

View file

@ -253,7 +253,7 @@ module.exports = React.createClass({
return ( return (
<div> <div>
<h3>Addresses</h3> <h3>{ _t('Addresses') }</h3>
<div className="mx_RoomSettings_aliasLabel"> <div className="mx_RoomSettings_aliasLabel">
{ _t('The main address for this room is') }: { canonical_alias_section } { _t('The main address for this room is') }: { canonical_alias_section }
</div> </div>

View file

@ -22,10 +22,11 @@ const MatrixClientPeg = require("../../../MatrixClientPeg");
const Modal = require("../../../Modal"); const Modal = require("../../../Modal");
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
const ROOM_COLORS = [ const ROOM_COLORS = [
// magic room default values courtesy of Ribot // magic room default values courtesy of Ribot
["#76cfa6", "#eaf5f0"], [Tinter.getKeyRgb()[0], Tinter.getKeyRgb()[1]],
["#81bddb", "#eaf1f4"], ["#81bddb", "#eaf1f4"],
["#bd79cb", "#f3eaf5"], ["#bd79cb", "#f3eaf5"],
["#c65d94", "#f5eaef"], ["#c65d94", "#f5eaef"],
@ -47,17 +48,17 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
const data = { const data = {
index: 0, index: 0,
primary_color: ROOM_COLORS[0].primary_color, primary_color: ROOM_COLORS[0][0],
secondary_color: ROOM_COLORS[0].secondary_color, secondary_color: ROOM_COLORS[0][1],
hasChanged: false, hasChanged: false,
}; };
const event = this.props.room.getAccountData("org.matrix.room.color_scheme"); const scheme = SettingsStore.getValueAt(SettingLevel.ROOM_ACCOUNT, "roomColor", this.props.room.roomId);
if (!event) {
return data; if (scheme.primary_color && scheme.secondary_color) {
} // We only use the user's scheme if the scheme is valid.
const scheme = event.getContent();
data.primary_color = scheme.primary_color; data.primary_color = scheme.primary_color;
data.secondary_color = scheme.secondary_color; data.secondary_color = scheme.secondary_color;
}
data.index = this._getColorIndex(data); data.index = this._getColorIndex(data);
if (data.index === -1) { if (data.index === -1) {
@ -81,13 +82,13 @@ module.exports = React.createClass({
// We would like guests to be able to set room colour but currently // We would like guests to be able to set room colour but currently
// they can't, so we still send the request but display a sensible // they can't, so we still send the request but display a sensible
// error if it fails. // error if it fails.
return MatrixClientPeg.get().setRoomAccountData( // TODO: Support guests for room color. Technically this is possible via granular settings
this.props.room.roomId, "org.matrix.room.color_scheme", { // Granular settings would mean the guest is forced to use the DEVICE level though.
SettingsStore.setValue("roomColor", this.props.room.roomId, SettingLevel.ROOM_ACCOUNT, {
primary_color: this.state.primary_color, primary_color: this.state.primary_color,
secondary_color: this.state.secondary_color, secondary_color: this.state.secondary_color,
}, }).catch(function(err) {
).catch(function(err) { if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
dis.dispatch({action: 'view_set_mxid'}); dis.dispatch({action: 'view_set_mxid'});
} }
}); });

View file

@ -104,8 +104,8 @@ module.exports = React.createClass({
render: function() { render: function() {
const localDomain = this.context.matrixClient.getDomain(); const localDomain = this.context.matrixClient.getDomain();
const EditableItemList = sdk.getComponent('elements.EditableItemList'); const EditableItemList = sdk.getComponent('elements.EditableItemList');
return (<div> return <div>
<h3>{ _t('Related Communities') }</h3> <h3>{ _t('Flair') }</h3>
<EditableItemList <EditableItemList
items={this.state.newGroupsList} items={this.state.newGroupsList}
className={"mx_RelatedGroupSettings"} className={"mx_RelatedGroupSettings"}
@ -115,12 +115,12 @@ module.exports = React.createClass({
onItemAdded={this.onGroupAdded} onItemAdded={this.onGroupAdded}
onItemEdited={this.onGroupEdited} onItemEdited={this.onGroupEdited}
onItemRemoved={this.onGroupDeleted} onItemRemoved={this.onGroupDeleted}
itemsLabel={_t('Related communities for this room:')} itemsLabel={_t('Showing flair for these communities:')}
noItemsLabel={_t('This room has no related communities')} noItemsLabel={_t('This room is not showing flair for any communities')}
placeholder={_t( placeholder={_t(
'New community ID (e.g. +foo:%(localDomain)s)', {localDomain}, 'New community ID (e.g. +foo:%(localDomain)s)', {localDomain},
)} )}
/> />
</div>); </div>;
}, },
}); });

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2017 Travis Ralston
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,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import Promise from 'bluebird';
const React = require('react'); const React = require('react');
const MatrixClientPeg = require('../../../MatrixClientPeg');
const sdk = require("../../../index"); const sdk = require("../../../index");
const Modal = require("../../../Modal"); import { _t, _td } from '../../../languageHandler';
const UserSettingsStore = require('../../../UserSettingsStore'); import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import { _t, _tJsx } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
@ -30,137 +28,64 @@ module.exports = React.createClass({
room: React.PropTypes.object, room: React.PropTypes.object,
}, },
getInitialState: function() {
const cli = MatrixClientPeg.get();
const roomState = this.props.room.currentState;
const roomPreviewUrls = this.props.room.currentState.getStateEvents('org.matrix.room.preview_urls', '');
const userPreviewUrls = this.props.room.getAccountData("org.matrix.room.preview_urls");
return {
globalDisableUrlPreview: (roomPreviewUrls && roomPreviewUrls.getContent().disable) || false,
userDisableUrlPreview: (userPreviewUrls && (userPreviewUrls.getContent().disable === true)) || false,
userEnableUrlPreview: (userPreviewUrls && (userPreviewUrls.getContent().disable === false)) || false,
};
},
componentDidMount: function() {
this.originalState = Object.assign({}, this.state);
},
saveSettings: function() { saveSettings: function() {
const promises = []; const promises = [];
if (this.refs.urlPreviewsRoom) promises.push(this.refs.urlPreviewsRoom.save());
if (this.state.globalDisableUrlPreview !== this.originalState.globalDisableUrlPreview) { if (this.refs.urlPreviewsSelf) promises.push(this.refs.urlPreviewsSelf.save());
console.log("UrlPreviewSettings: Updating room's preview_urls state event");
promises.push(
MatrixClientPeg.get().sendStateEvent(
this.props.room.roomId, "org.matrix.room.preview_urls", {
disable: this.state.globalDisableUrlPreview,
}, "",
),
);
}
let content = undefined;
if (this.state.userDisableUrlPreview !== this.originalState.userDisableUrlPreview) {
console.log("UrlPreviewSettings: Disabling user's per-room preview_urls");
content = this.state.userDisableUrlPreview ? { disable: true } : {};
}
if (this.state.userEnableUrlPreview !== this.originalState.userEnableUrlPreview) {
console.log("UrlPreviewSettings: Enabling user's per-room preview_urls");
if (!content || content.disable === undefined) {
content = this.state.userEnableUrlPreview ? { disable: false } : {};
}
}
if (content) {
promises.push(
MatrixClientPeg.get().setRoomAccountData(
this.props.room.roomId, "org.matrix.room.preview_urls", content,
),
);
}
console.log("UrlPreviewSettings: saveSettings: " + JSON.stringify(promises));
return promises; return promises;
}, },
onGlobalDisableUrlPreviewChange: function() {
this.setState({
globalDisableUrlPreview: this.refs.globalDisableUrlPreview.checked ? true : false,
});
},
onUserEnableUrlPreviewChange: function() {
this.setState({
userDisableUrlPreview: false,
userEnableUrlPreview: this.refs.userEnableUrlPreview.checked ? true : false,
});
},
onUserDisableUrlPreviewChange: function() {
this.setState({
userDisableUrlPreview: this.refs.userDisableUrlPreview.checked ? true : false,
userEnableUrlPreview: false,
});
},
render: function() { render: function() {
const self = this; const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
const roomState = this.props.room.currentState; const roomId = this.props.room.roomId;
const cli = MatrixClientPeg.get();
const maySetRoomPreviewUrls = roomState.mayClientSendStateEvent('org.matrix.room.preview_urls', cli); let previewsForAccount = null;
let disableRoomPreviewUrls; if (SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled")) {
if (maySetRoomPreviewUrls) { previewsForAccount = (
disableRoomPreviewUrls = _t("You have <a>enabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
<label>
<input type="checkbox" ref="globalDisableUrlPreview"
onChange={this.onGlobalDisableUrlPreviewChange}
checked={this.state.globalDisableUrlPreview} />
{ _t("Disable URL previews by default for participants in this room") }
</label>;
} else {
disableRoomPreviewUrls =
<label>
{ _t("URL previews are %(globalDisableUrlPreview)s by default for participants in this room.", {globalDisableUrlPreview: this.state.globalDisableUrlPreview ? _t("disabled") : _t("enabled")}) }
</label>;
}
let urlPreviewText = null;
if (UserSettingsStore.getUrlPreviewsDisabled()) {
urlPreviewText = (
_tJsx("You have <a>disabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{ sub }</a>)
); );
} else { } else {
urlPreviewText = ( previewsForAccount = (
_tJsx("You have <a>enabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{ sub }</a>) _t("You have <a>disabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
); );
} }
let previewsForRoom = null;
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
previewsForRoom = (
<label>
<SettingsFlag name="urlPreviewsEnabled"
level={SettingLevel.ROOM}
roomId={this.props.room.roomId}
isExplicit={true}
manualSave={true}
ref="urlPreviewsRoom" />
</label>
);
} else {
let str = _td("URL previews are enabled by default for participants in this room.");
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/true)) {
str = _td("URL previews are disabled by default for participants in this room.");
}
previewsForRoom = (<label>{ _t(str) }</label>);
}
const previewsForRoomAccount = (
<SettingsFlag name="urlPreviewsEnabled"
level={SettingLevel.ROOM_ACCOUNT}
roomId={this.props.room.roomId}
manualSave={true}
ref="urlPreviewsSelf"
/>
);
return ( return (
<div className="mx_RoomSettings_toggles"> <div className="mx_RoomSettings_toggles">
<h3>{ _t("URL Previews") }</h3> <h3>{ _t("URL Previews") }</h3>
<label> <label>{ previewsForAccount }</label>
{ urlPreviewText } { previewsForRoom }
</label> <label>{ previewsForRoomAccount }</label>
{ disableRoomPreviewUrls }
<label>
<input type="checkbox" ref="userEnableUrlPreview"
onChange={this.onUserEnableUrlPreviewChange}
checked={this.state.userEnableUrlPreview} />
{ _t("Enable URL previews for this room (affects only you)") }
</label>
<label>
<input type="checkbox" ref="userDisableUrlPreview"
onChange={this.onUserDisableUrlPreviewChange}
checked={this.state.userDisableUrlPreview} />
{ _t("Disable URL previews for this room (affects only you)") }
</label>
</div> </div>
); );
}, },

View file

@ -27,6 +27,7 @@ import ScalarAuthClient from '../../../ScalarAuthClient';
import ScalarMessaging from '../../../ScalarMessaging'; import ScalarMessaging from '../../../ScalarMessaging';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import WidgetUtils from '../../../WidgetUtils'; import WidgetUtils from '../../../WidgetUtils';
import SettingsStore from "../../../settings/SettingsStore";
// The maximum number of widgets that can be added in a room // The maximum number of widgets that can be added in a room
const MAX_WIDGETS = 2; const MAX_WIDGETS = 2;
@ -131,16 +132,22 @@ module.exports = React.createClass({
'$matrix_room_id': this.props.room.roomId, '$matrix_room_id': this.props.room.roomId,
'$matrix_display_name': user ? user.displayName : this.props.userId, '$matrix_display_name': user ? user.displayName : this.props.userId,
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '', '$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
// TODO: Namespace themes through some standard
'$theme': SettingsStore.getValue("theme"),
}; };
app.id = appId;
app.name = app.name || app.type;
if (app.data) { if (app.data) {
Object.keys(app.data).forEach((key) => { Object.keys(app.data).forEach((key) => {
params['$' + key] = app.data[key]; params['$' + key] = app.data[key];
}); });
app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true);
} }
app.id = appId;
app.name = app.name || app.type;
app.url = this.encodeUri(app.url, params); app.url = this.encodeUri(app.url, params);
app.creatorUserId = (sender && sender.userId) ? sender.userId : null; app.creatorUserId = (sender && sender.userId) ? sender.userId : null;
@ -224,6 +231,8 @@ module.exports = React.createClass({
userId={this.props.userId} userId={this.props.userId}
show={this.props.showApps} show={this.props.showApps}
creatorUserId={app.creatorUserId} creatorUserId={app.creatorUserId}
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
waitForIframeLoad={app.waitForIframeLoad}
/>); />);
}); });

View file

@ -24,9 +24,10 @@ import isEqual from 'lodash/isEqual';
import sdk from '../../../index'; import sdk from '../../../index';
import type {Completion} from '../../../autocomplete/Autocompleter'; import type {Completion} from '../../../autocomplete/Autocompleter';
import Promise from 'bluebird'; import Promise from 'bluebird';
import UserSettingsStore from '../../../UserSettingsStore';
import { Room } from 'matrix-js-sdk'; import { Room } from 'matrix-js-sdk';
import {getCompletions} from '../../../autocomplete/Autocompleter';
import SettingsStore from "../../../settings/SettingsStore";
import Autocompleter from '../../../autocomplete/Autocompleter'; import Autocompleter from '../../../autocomplete/Autocompleter';
const COMPOSER_SELECTED = 0; const COMPOSER_SELECTED = 0;
@ -95,7 +96,7 @@ export default class Autocomplete extends React.Component {
}); });
return Promise.resolve(null); return Promise.resolve(null);
} }
let autocompleteDelay = UserSettingsStore.getLocalSetting('autocompleteDelay', 200); let autocompleteDelay = SettingsStore.getValue("autocompleteDelay");
// Don't debounce if we are already showing completions // Don't debounce if we are already showing completions
if (this.state.completions.length > 0 || this.state.forceComplete) { if (this.state.completions.length > 0 || this.state.forceComplete) {

View file

@ -21,8 +21,7 @@ import sdk from '../../../index';
import dis from "../../../dispatcher"; import dis from "../../../dispatcher";
import ObjectUtils from '../../../ObjectUtils'; import ObjectUtils from '../../../ObjectUtils';
import AppsDrawer from './AppsDrawer'; import AppsDrawer from './AppsDrawer';
import { _t, _tJsx} from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import UserSettingsStore from '../../../UserSettingsStore';
module.exports = React.createClass({ module.exports = React.createClass({
@ -100,13 +99,13 @@ module.exports = React.createClass({
supportedText = _t(" (unsupported)"); supportedText = _t(" (unsupported)");
} else { } else {
joinNode = (<span> joinNode = (<span>
{ _tJsx( { _t(
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.", "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
[/<voiceText>(.*?)<\/voiceText>/, /<videoText>(.*?)<\/videoText>/], {},
[ {
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }</a>, 'voiceText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }</a>,
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }</a>, 'videoText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }</a>,
], },
) } ) }
</span>); </span>);
} }

View file

@ -33,22 +33,30 @@ const ObjectUtils = require('../../../ObjectUtils');
const eventTileTypes = { const eventTileTypes = {
'm.room.message': 'messages.MessageEvent', 'm.room.message': 'messages.MessageEvent',
'm.room.member': 'messages.TextualEvent',
'm.call.invite': 'messages.TextualEvent', 'm.call.invite': 'messages.TextualEvent',
'm.call.answer': 'messages.TextualEvent', 'm.call.answer': 'messages.TextualEvent',
'm.call.hangup': 'messages.TextualEvent', 'm.call.hangup': 'messages.TextualEvent',
};
const stateEventTileTypes = {
'm.room.member': 'messages.TextualEvent',
'm.room.name': 'messages.TextualEvent', 'm.room.name': 'messages.TextualEvent',
'm.room.avatar': 'messages.RoomAvatarEvent', 'm.room.avatar': 'messages.RoomAvatarEvent',
'm.room.topic': 'messages.TextualEvent',
'm.room.third_party_invite': 'messages.TextualEvent', 'm.room.third_party_invite': 'messages.TextualEvent',
'm.room.history_visibility': 'messages.TextualEvent', 'm.room.history_visibility': 'messages.TextualEvent',
'm.room.encryption': 'messages.TextualEvent', 'm.room.encryption': 'messages.TextualEvent',
'm.room.topic': 'messages.TextualEvent',
'm.room.power_levels': 'messages.TextualEvent', 'm.room.power_levels': 'messages.TextualEvent',
'm.room.pinned_events': 'messages.TextualEvent', 'm.room.pinned_events': 'messages.TextualEvent',
'im.vector.modular.widgets': 'messages.TextualEvent', 'im.vector.modular.widgets': 'messages.TextualEvent',
}; };
function getHandlerTile(ev) {
const type = ev.getType();
return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type];
}
const MAX_READ_AVATARS = 5; const MAX_READ_AVATARS = 5;
// Our component structure for EventTiles on the timeline is: // Our component structure for EventTiles on the timeline is:
@ -188,6 +196,8 @@ module.exports = withMatrixClient(React.createClass({
*/ */
_onDecrypted: function() { _onDecrypted: function() {
// we need to re-verify the sending device. // we need to re-verify the sending device.
// (we call onWidgetLoad in _verifyEvent to handle the case where decryption
// has caused a change in size of the event tile)
this._verifyEvent(this.props.mxEvent); this._verifyEvent(this.props.mxEvent);
this.forceUpdate(); this.forceUpdate();
}, },
@ -206,6 +216,9 @@ module.exports = withMatrixClient(React.createClass({
const verified = await this.props.matrixClient.isEventSenderVerified(mxEvent); const verified = await this.props.matrixClient.isEventSenderVerified(mxEvent);
this.setState({ this.setState({
verified: verified, verified: verified,
}, () => {
// Decryption may have caused a change in size
this.props.onWidgetLoad();
}); });
}, },
@ -433,7 +446,7 @@ module.exports = withMatrixClient(React.createClass({
// Info messages are basically information about commands processed on a room // Info messages are basically information about commands processed on a room
const isInfoMessage = (eventType !== 'm.room.message'); const isInfoMessage = (eventType !== 'm.room.message');
const EventTileType = sdk.getComponent(eventTileTypes[eventType]); const EventTileType = sdk.getComponent(getHandlerTile(this.props.mxEvent));
// This shouldn't happen: the caller should check we support this type // This shouldn't happen: the caller should check we support this type
// before trying to instantiate us // before trying to instantiate us
if (!EventTileType) { if (!EventTileType) {
@ -600,8 +613,10 @@ module.exports = withMatrixClient(React.createClass({
module.exports.haveTileForEvent = function(e) { module.exports.haveTileForEvent = function(e) {
// Only messages have a tile (black-rectangle) if redacted // Only messages have a tile (black-rectangle) if redacted
if (e.isRedacted() && e.getType() !== 'm.room.message') return false; if (e.isRedacted() && e.getType() !== 'm.room.message') return false;
if (eventTileTypes[e.getType()] == undefined) return false;
if (eventTileTypes[e.getType()] == 'messages.TextualEvent') { const handler = getHandlerTile(e);
if (handler === undefined) return false;
if (handler === 'messages.TextualEvent') {
return TextForEvent.textForEvent(e) !== ''; return TextForEvent.textForEvent(e) !== '';
} else { } else {
return true; return true;

View file

@ -18,7 +18,7 @@
import React from 'react'; import React from 'react';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import KeyCode from '../../../KeyCode'; import { KeyCode } from '../../../Keyboard';
module.exports = React.createClass({ module.exports = React.createClass({

View file

@ -494,7 +494,6 @@ module.exports = withMatrixClient(React.createClass({
const defaultPerms = { const defaultPerms = {
can: {}, can: {},
muted: false, muted: false,
modifyLevel: false,
}; };
const room = this.props.matrixClient.getRoom(member.roomId); const room = this.props.matrixClient.getRoom(member.roomId);
if (!room) return defaultPerms; if (!room) return defaultPerms;
@ -516,13 +515,15 @@ module.exports = withMatrixClient(React.createClass({
}, },
_calculateCanPermissions: function(me, them, powerLevels) { _calculateCanPermissions: function(me, them, powerLevels) {
const isMe = me.userId === them.userId;
const can = { const can = {
kick: false, kick: false,
ban: false, ban: false,
mute: false, mute: false,
modifyLevel: false, modifyLevel: false,
modifyLevelMax: 0,
}; };
const canAffectUser = them.powerLevel < me.powerLevel; const canAffectUser = them.powerLevel < me.powerLevel || isMe;
if (!canAffectUser) { if (!canAffectUser) {
//console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel); //console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
return can; return can;
@ -531,16 +532,13 @@ module.exports = withMatrixClient(React.createClass({
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
powerLevels.state_default powerLevels.state_default
); );
const levelToSend = (
(powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
powerLevels.events_default
);
can.kick = me.powerLevel >= powerLevels.kick; can.kick = me.powerLevel >= powerLevels.kick;
can.ban = me.powerLevel >= powerLevels.ban; can.ban = me.powerLevel >= powerLevels.ban;
can.mute = me.powerLevel >= editPowerLevel; can.mute = me.powerLevel >= editPowerLevel;
can.toggleMod = me.powerLevel > them.powerLevel && them.powerLevel >= levelToSend; can.modifyLevel = me.powerLevel >= editPowerLevel && (isMe || me.powerLevel > them.powerLevel);
can.modifyLevel = me.powerLevel > them.powerLevel && me.powerLevel >= editPowerLevel; can.modifyLevelMax = me.powerLevel;
return can; return can;
}, },
@ -832,8 +830,11 @@ module.exports = withMatrixClient(React.createClass({
presenceCurrentlyActive = this.props.member.user.currentlyActive; presenceCurrentlyActive = this.props.member.user.currentlyActive;
} }
let roomMemberDetails = null; const room = this.props.matrixClient.getRoom(this.props.member.roomId);
const poweLevelEvent = room ? room.currentState.getStateEvents("m.room.power_levels", "") : null;
const powerLevelUsersDefault = poweLevelEvent.getContent().users_default;
let roomMemberDetails = null;
if (this.props.member.roomId) { // is in room if (this.props.member.roomId) { // is in room
const PowerSelector = sdk.getComponent('elements.PowerSelector'); const PowerSelector = sdk.getComponent('elements.PowerSelector');
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel'); const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
@ -842,7 +843,9 @@ module.exports = withMatrixClient(React.createClass({
{ _t("Level:") } <b> { _t("Level:") } <b>
<PowerSelector controlled={true} <PowerSelector controlled={true}
value={parseInt(this.props.member.powerLevel)} value={parseInt(this.props.member.powerLevel)}
maxValue={this.state.can.modifyLevelMax}
disabled={!this.state.can.modifyLevel} disabled={!this.state.can.modifyLevel}
usersDefault={powerLevelUsersDefault}
onChange={this.onPowerChange} /> onChange={this.onPowerChange} />
</b> </b>
</div> </div>

View file

@ -22,7 +22,7 @@ import Modal from '../../../Modal';
import sdk from '../../../index'; import sdk from '../../../index';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import Autocomplete from './Autocomplete'; import Autocomplete from './Autocomplete';
import UserSettingsStore from '../../../UserSettingsStore'; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
export default class MessageComposer extends React.Component { export default class MessageComposer extends React.Component {
@ -49,10 +49,10 @@ export default class MessageComposer extends React.Component {
inputState: { inputState: {
style: [], style: [],
blockType: null, blockType: null,
isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false), isRichtextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
wordCount: 0, wordCount: 0,
}, },
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false), showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
}; };
} }
@ -226,7 +226,7 @@ export default class MessageComposer extends React.Component {
} }
onToggleFormattingClicked() { onToggleFormattingClicked() {
UserSettingsStore.setSyncedSetting('MessageComposer.showFormatting', !this.state.showFormatting); SettingsStore.setValue("MessageComposer.showFormatting", null, SettingLevel.DEVICE, !this.state.showFormatting);
this.setState({showFormatting: !this.state.showFormatting}); this.setState({showFormatting: !this.state.showFormatting});
} }
@ -238,7 +238,7 @@ export default class MessageComposer extends React.Component {
render() { render() {
const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
const uploadInputStyle = {display: 'none'}; const uploadInputStyle = {display: 'none'};
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); const MemberPresenceAvatar = sdk.getComponent('avatars.MemberPresenceAvatar');
const TintableSvg = sdk.getComponent("elements.TintableSvg"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput"); const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput");
@ -246,7 +246,7 @@ export default class MessageComposer extends React.Component {
controls.push( controls.push(
<div key="controls_avatar" className="mx_MessageComposer_avatar"> <div key="controls_avatar" className="mx_MessageComposer_avatar">
<MemberAvatar member={me} width={24} height={24} /> <MemberPresenceAvatar member={me} width={24} height={24} />
</div>, </div>,
); );

View file

@ -28,14 +28,13 @@ import Promise from 'bluebird';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix'; import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
import SlashCommands from '../../../SlashCommands'; import SlashCommands from '../../../SlashCommands';
import KeyCode from '../../../KeyCode'; import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import Analytics from '../../../Analytics'; import Analytics from '../../../Analytics';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import UserSettingsStore from '../../../UserSettingsStore';
import * as RichText from '../../../RichText'; import * as RichText from '../../../RichText';
import * as HtmlUtils from '../../../HtmlUtils'; import * as HtmlUtils from '../../../HtmlUtils';
@ -50,6 +49,7 @@ const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g'); const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g');
import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione'; import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
const EMOJI_SHORTNAMES = Object.keys(emojioneList); const EMOJI_SHORTNAMES = Object.keys(emojioneList);
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort(); const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$'); const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
@ -74,13 +74,6 @@ function onSendMessageFailed(err, room) {
// XXX: temporary logging to try to diagnose // XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148 // https://github.com/vector-im/riot-web/issues/3148
console.log('MessageComposer got send failure: ' + err.name + '('+err+')'); console.log('MessageComposer got send failure: ' + err.name + '('+err+')');
if (err.name === "UnknownDeviceError") {
dis.dispatch({
action: 'unknown_device_error',
err: err,
room: room,
});
}
dis.dispatch({ dis.dispatch({
action: 'message_send_failed', action: 'message_send_failed',
}); });
@ -105,13 +98,7 @@ export default class MessageComposerInput extends React.Component {
}; };
static getKeyBinding(ev: SyntheticKeyboardEvent): string { static getKeyBinding(ev: SyntheticKeyboardEvent): string {
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
let ctrlCmdOnly;
if (isMac) {
ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
} else {
ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
}
// Restrict a subset of key bindings to ONLY having ctrl/meta* pressed and // Restrict a subset of key bindings to ONLY having ctrl/meta* pressed and
// importantly NOT having alt, shift, meta/ctrl* pressed. draft-js does not // importantly NOT having alt, shift, meta/ctrl* pressed. draft-js does not
@ -165,7 +152,7 @@ export default class MessageComposerInput extends React.Component {
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this); this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
this.onTextPasted = this.onTextPasted.bind(this); this.onTextPasted = this.onTextPasted.bind(this);
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false); const isRichtextEnabled = SettingsStore.getValue('MessageComposerInput.isRichTextEnabled');
Analytics.setRichtextMode(isRichtextEnabled); Analytics.setRichtextMode(isRichtextEnabled);
@ -216,7 +203,7 @@ export default class MessageComposerInput extends React.Component {
createEditorState(richText: boolean, contentState: ?ContentState): EditorState { createEditorState(richText: boolean, contentState: ?ContentState): EditorState {
const decorators = richText ? RichText.getScopedRTDecorators(this.props) : const decorators = richText ? RichText.getScopedRTDecorators(this.props) :
RichText.getScopedMDDecorators(this.props); RichText.getScopedMDDecorators(this.props);
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false); const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
decorators.push({ decorators.push({
strategy: this.findPillEntities.bind(this), strategy: this.findPillEntities.bind(this),
component: (entityProps) => { component: (entityProps) => {
@ -384,7 +371,7 @@ export default class MessageComposerInput extends React.Component {
} }
sendTyping(isTyping) { sendTyping(isTyping) {
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return; if (SettingsStore.getValue('dontSendTypingNotifications')) return;
MatrixClientPeg.get().sendTyping( MatrixClientPeg.get().sendTyping(
this.props.room.roomId, this.props.room.roomId,
this.isTyping, TYPING_SERVER_TIMEOUT, this.isTyping, TYPING_SERVER_TIMEOUT,
@ -431,7 +418,7 @@ export default class MessageComposerInput extends React.Component {
} }
// Automatic replacement of plaintext emoji to Unicode emoji // Automatic replacement of plaintext emoji to Unicode emoji
if (UserSettingsStore.getSyncedSetting('MessageComposerInput.autoReplaceEmoji', false)) { if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
// The first matched group includes just the matched plaintext emoji // The first matched group includes just the matched plaintext emoji
const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset)); const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset));
if (emojiMatch) { if (emojiMatch) {
@ -552,7 +539,7 @@ export default class MessageComposerInput extends React.Component {
editorState: this.createEditorState(enabled, contentState), editorState: this.createEditorState(enabled, contentState),
isRichtextEnabled: enabled, isRichtextEnabled: enabled,
}); });
UserSettingsStore.setSyncedSetting('MessageComposerInput.isRichTextEnabled', enabled); SettingsStore.setValue("MessageComposerInput.isRichTextEnabled", null, SettingLevel.ACCOUNT, enabled);
} }
handleKeyCommand = (command: string): boolean => { handleKeyCommand = (command: string): boolean => {

View file

@ -44,6 +44,8 @@ module.exports = React.createClass({
}; };
}, },
// Return duration as a string using appropriate time units
// XXX: This would be better handled using a culture-aware library, but we don't use one yet.
getDuration: function(time) { getDuration: function(time) {
if (!time) return; if (!time) return;
const t = parseInt(time / 1000); const t = parseInt(time / 1000);
@ -53,41 +55,39 @@ module.exports = React.createClass({
const d = parseInt(t / (60 * 60 * 24)); const d = parseInt(t / (60 * 60 * 24));
if (t < 60) { if (t < 60) {
if (t < 0) { if (t < 0) {
return _t("for %(amount)ss", {amount: 0}); return _t("%(duration)ss", {duration: 0});
} }
return _t("for %(amount)ss", {amount: s}); return _t("%(duration)ss", {duration: s});
} }
if (t < 60 * 60) { if (t < 60 * 60) {
return _t("for %(amount)sm", {amount: m}); return _t("%(duration)sm", {duration: m});
} }
if (t < 24 * 60 * 60) { if (t < 24 * 60 * 60) {
return _t("for %(amount)sh", {amount: h}); return _t("%(duration)sh", {duration: h});
} }
return _t("for %(amount)sd", {amount: d}); return _t("%(duration)sd", {duration: d});
}, },
getPrettyPresence: function(presence) { getPrettyPresence: function(presence, activeAgo, currentlyActive) {
if (!currentlyActive && activeAgo !== undefined && activeAgo > 0) {
const duration = this.getDuration(activeAgo);
if (presence === "online") return _t("Online for %(duration)s", { duration: duration });
if (presence === "unavailable") return _t("Idle for %(duration)s", { duration: duration }); // XXX: is this actually right?
if (presence === "offline") return _t("Offline for %(duration)s", { duration: duration });
return _t("Unknown for %(duration)s", { duration: duration });
} else {
if (presence === "online") return _t("Online"); if (presence === "online") return _t("Online");
if (presence === "unavailable") return _t("Idle"); // XXX: is this actually right? if (presence === "unavailable") return _t("Idle"); // XXX: is this actually right?
if (presence === "offline") return _t("Offline"); if (presence === "offline") return _t("Offline");
return _t("Unknown"); return _t("Unknown");
}
}, },
render: function() { render: function() {
if (this.props.activeAgo >= 0) {
const duration = this.getDuration(this.props.activeAgo);
const ago = this.props.currentlyActive || !duration ? "" : duration;
return ( return (
<div className="mx_PresenceLabel"> <div className="mx_PresenceLabel">
{ this.getPrettyPresence(this.props.presenceState) } { ago } { this.getPrettyPresence(this.props.presenceState, this.props.activeAgo, this.props.currentlyActive) }
</div> </div>
); );
} else {
return (
<div className="mx_PresenceLabel">
{ this.getPrettyPresence(this.props.presenceState) }
</div>
);
}
}, },
}); });

View file

@ -31,7 +31,7 @@ import linkifyMatrix from '../../../linkify-matrix';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import ManageIntegsButton from '../elements/ManageIntegsButton'; import ManageIntegsButton from '../elements/ManageIntegsButton';
import {CancelButton} from './SimpleRoomHeader'; import {CancelButton} from './SimpleRoomHeader';
import UserSettingsStore from "../../../UserSettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
linkifyMatrix(linkify); linkifyMatrix(linkify);
@ -339,7 +339,7 @@ module.exports = React.createClass({
</AccessibleButton>; </AccessibleButton>;
} }
if (this.props.onPinnedClick && UserSettingsStore.isFeatureEnabled('feature_pinning')) { if (this.props.onPinnedClick && SettingsStore.isFeatureEnabled('feature_pinning')) {
let pinsIndicator = null; let pinsIndicator = null;
if (this._hasUnreadPins()) { if (this._hasUnreadPins()) {
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />); pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />);

View file

@ -18,18 +18,18 @@ limitations under the License.
'use strict'; 'use strict';
const React = require("react"); const React = require("react");
const ReactDOM = require("react-dom"); const ReactDOM = require("react-dom");
import { _t, _tJsx } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
const GeminiScrollbar = require('react-gemini-scrollbar'); const GeminiScrollbar = require('react-gemini-scrollbar');
const MatrixClientPeg = require("../../../MatrixClientPeg"); const MatrixClientPeg = require("../../../MatrixClientPeg");
const CallHandler = require('../../../CallHandler'); const CallHandler = require('../../../CallHandler');
const RoomListSorter = require("../../../RoomListSorter");
const Unread = require('../../../Unread');
const dis = require("../../../dispatcher"); const dis = require("../../../dispatcher");
const sdk = require('../../../index'); const sdk = require('../../../index');
const rate_limited_func = require('../../../ratelimitedfunc'); const rate_limited_func = require('../../../ratelimitedfunc');
const Rooms = require('../../../Rooms'); const Rooms = require('../../../Rooms');
import DMRoomMap from '../../../utils/DMRoomMap'; import DMRoomMap from '../../../utils/DMRoomMap';
const Receipt = require('../../../utils/Receipt'); const Receipt = require('../../../utils/Receipt');
import FilterStore from '../../../stores/FilterStore';
import GroupStoreCache from '../../../stores/GroupStoreCache';
const HIDE_CONFERENCE_CHANS = true; const HIDE_CONFERENCE_CHANS = true;
@ -63,6 +63,7 @@ module.exports = React.createClass({
totalRoomCount: null, totalRoomCount: null,
lists: {}, lists: {},
incomingCall: null, incomingCall: null,
selectedTags: [],
}; };
}, },
@ -70,6 +71,7 @@ module.exports = React.createClass({
this.mounted = false; this.mounted = false;
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
cli.on("Room", this.onRoom); cli.on("Room", this.onRoom);
cli.on("deleteRoom", this.onDeleteRoom); cli.on("deleteRoom", this.onDeleteRoom);
cli.on("Room.timeline", this.onRoomTimeline); cli.on("Room.timeline", this.onRoomTimeline);
@ -82,6 +84,35 @@ module.exports = React.createClass({
cli.on("accountData", this.onAccountData); cli.on("accountData", this.onAccountData);
cli.on("Group.myMembership", this._onGroupMyMembership); cli.on("Group.myMembership", this._onGroupMyMembership);
const dmRoomMap = DMRoomMap.shared();
this._groupStores = {};
this._groupStoreTokens = [];
// A map between tags which are group IDs and the room IDs of rooms that should be kept
// in the room list when filtering by that tag.
this._visibleRoomsForGroup = {
// $groupId: [$roomId1, $roomId2, ...],
};
// All rooms that should be kept in the room list when filtering
this._visibleRooms = [];
// When the selected tags are changed, initialise a group store if necessary
this._filterStoreToken = FilterStore.addListener(() => {
FilterStore.getSelectedTags().forEach((tag) => {
if (tag[0] !== '+' || this._groupStores[tag]) {
return;
}
this._groupStores[tag] = GroupStoreCache.getGroupStore(tag);
this._groupStoreTokens.push(
this._groupStores[tag].registerListener(() => {
// This group's rooms or members may have updated, update rooms for its tag
this.updateVisibleRoomsForTag(dmRoomMap, tag);
this.updateVisibleRooms();
}),
);
});
// Filters themselves have changed, refresh the selected tags
this.updateVisibleRooms();
});
this.refreshRoomList(); this.refreshRoomList();
// order of the sublists // order of the sublists
@ -150,6 +181,16 @@ module.exports = React.createClass({
MatrixClientPeg.get().removeListener("accountData", this.onAccountData); MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership); MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
} }
if (this._filterStoreToken) {
this._filterStoreToken.remove();
}
if (this._groupStoreTokens.length > 0) {
// NB: GroupStore is not a Flux.Store
this._groupStoreTokens.forEach((token) => token.unregister());
}
// cancel any pending calls to the rate_limited_funcs // cancel any pending calls to the rate_limited_funcs
this._delayedRefreshRoomList.cancelPendingCall(); this._delayedRefreshRoomList.cancelPendingCall();
}, },
@ -236,6 +277,45 @@ module.exports = React.createClass({
this.refreshRoomList(); this.refreshRoomList();
}, 500), }, 500),
// Update which rooms and users should appear in RoomList for a given group tag
updateVisibleRoomsForTag: function(dmRoomMap, tag) {
if (!this.mounted) return;
// For now, only handle group tags
const store = this._groupStores[tag];
if (!store) return;
this._visibleRoomsForGroup[tag] = [];
store.getGroupRooms().forEach((room) => this._visibleRoomsForGroup[tag].push(room.roomId));
store.getGroupMembers().forEach((member) => {
if (member.userId === MatrixClientPeg.get().credentials.userId) return;
dmRoomMap.getDMRoomsForUserId(member.userId).forEach(
(roomId) => this._visibleRoomsForGroup[tag].push(roomId),
);
});
// TODO: Check if room has been tagged to the group by the user
},
// Update which rooms and users should appear according to which tags are selected
updateVisibleRooms: function() {
this._visibleRooms = [];
FilterStore.getSelectedTags().forEach((tag) => {
(this._visibleRoomsForGroup[tag] || []).forEach(
(roomId) => this._visibleRooms.push(roomId),
);
});
this.setState({
selectedTags: FilterStore.getSelectedTags(),
}, () => {
this.refreshRoomList();
});
},
isRoomInSelectedTags: function(room) {
// No selected tags = every room is visible in the list
return this.state.selectedTags.length === 0 || this._visibleRooms.includes(room.roomId);
},
refreshRoomList: function() { refreshRoomList: function() {
// TODO: ideally we'd calculate this once at start, and then maintain // TODO: ideally we'd calculate this once at start, and then maintain
// any changes to it incrementally, updating the appropriate sublists // any changes to it incrementally, updating the appropriate sublists
@ -255,9 +335,7 @@ module.exports = React.createClass({
}, },
getRoomLists: function() { getRoomLists: function() {
const self = this;
const lists = {}; const lists = {};
lists["im.vector.fake.invite"] = []; lists["im.vector.fake.invite"] = [];
lists["m.favourite"] = []; lists["m.favourite"] = [];
lists["im.vector.fake.recent"] = []; lists["im.vector.fake.recent"] = [];
@ -265,9 +343,8 @@ module.exports = React.createClass({
lists["m.lowpriority"] = []; lists["m.lowpriority"] = [];
lists["im.vector.fake.archived"] = []; lists["im.vector.fake.archived"] = [];
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); const dmRoomMap = DMRoomMap.shared();
MatrixClientPeg.get().getRooms().forEach((room) => {
MatrixClientPeg.get().getRooms().forEach(function(room) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId); const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (!me) return; if (!me) return;
@ -278,13 +355,18 @@ module.exports = React.createClass({
if (me.membership == "invite") { if (me.membership == "invite") {
lists["im.vector.fake.invite"].push(room); lists["im.vector.fake.invite"].push(room);
} else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) { } else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, this.props.ConferenceHandler)) {
// skip past this room & don't put it in any lists // skip past this room & don't put it in any lists
} else if (me.membership == "join" || me.membership === "ban" || } else if (me.membership == "join" || me.membership === "ban" ||
(me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) { (me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) {
// Used to split rooms via tags // Used to split rooms via tags
const tagNames = Object.keys(room.tags); const tagNames = Object.keys(room.tags);
// Apply TagPanel filtering, derived from FilterStore
if (!this.isRoomInSelectedTags(room)) {
return;
}
if (tagNames.length) { if (tagNames.length) {
for (let i = 0; i < tagNames.length; i++) { for (let i = 0; i < tagNames.length; i++) {
const tagName = tagNames[i]; const tagName = tagNames[i];
@ -476,6 +558,10 @@ module.exports = React.createClass({
}, },
_getEmptyContent: function(section) { _getEmptyContent: function(section) {
if (this.state.selectedTags.length > 0) {
return null;
}
const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget'); const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
if (this.props.collapsed) { if (this.props.collapsed) {
@ -486,28 +572,25 @@ module.exports = React.createClass({
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton'); const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton'); const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
switch (section) { switch (section) {
case 'im.vector.fake.direct': case 'im.vector.fake.direct':
return <div className="mx_RoomList_emptySubListTip"> return <div className="mx_RoomList_emptySubListTip">
{ _tJsx( { _t(
"Press <StartChatButton> to start a chat with someone", "Press <StartChatButton> to start a chat with someone",
[/<StartChatButton>/], {},
[ { 'StartChatButton': <StartChatButton size="16" callout={true} /> },
(sub) => <StartChatButton size="16" callout={true} />,
],
) } ) }
</div>; </div>;
case 'im.vector.fake.recent': case 'im.vector.fake.recent':
return <div className="mx_RoomList_emptySubListTip"> return <div className="mx_RoomList_emptySubListTip">
{ _tJsx( { _t(
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or"+ "You're not in any rooms yet! Press <CreateRoomButton> to make a room or"+
" <RoomDirectoryButton> to browse the directory", " <RoomDirectoryButton> to browse the directory",
[/<CreateRoomButton>/, /<RoomDirectoryButton>/], {},
[ {
(sub) => <CreateRoomButton size="16" callout={true} />, 'CreateRoomButton': <CreateRoomButton size="16" callout={true} />,
(sub) => <RoomDirectoryButton size="16" callout={true} />, 'RoomDirectoryButton': <RoomDirectoryButton size="16" callout={true} />,
], },
) } ) }
</div>; </div>;
} }

View file

@ -21,7 +21,7 @@ const React = require('react');
const sdk = require('../../../index'); const sdk = require('../../../index');
const MatrixClientPeg = require('../../../MatrixClientPeg'); const MatrixClientPeg = require('../../../MatrixClientPeg');
import { _t, _tJsx } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RoomPreviewBar', displayName: 'RoomPreviewBar',
@ -135,13 +135,13 @@ module.exports = React.createClass({
{ _t('You have been invited to join this room by %(inviterName)s', {inviterName: this.props.inviterName}) } { _t('You have been invited to join this room by %(inviterName)s', {inviterName: this.props.inviterName}) }
</div> </div>
<div className="mx_RoomPreviewBar_join_text"> <div className="mx_RoomPreviewBar_join_text">
{ _tJsx( { _t(
'Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?', 'Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?',
[/<acceptText>(.*?)<\/acceptText>/, /<declineText>(.*?)<\/declineText>/], {},
[ {
(sub) => <a onClick={this.props.onJoinClick}>{ sub }</a>, 'acceptText': (sub) => <a onClick={this.props.onJoinClick}>{ sub }</a>,
(sub) => <a onClick={this.props.onRejectClick}>{ sub }</a>, 'declineText': (sub) => <a onClick={this.props.onRejectClick}>{ sub }</a>,
], },
) } ) }
</div> </div>
{ emailMatchBlock } { emailMatchBlock }
@ -211,9 +211,9 @@ module.exports = React.createClass({
<div className="mx_RoomPreviewBar_join_text"> <div className="mx_RoomPreviewBar_join_text">
{ name ? _t('You are trying to access %(roomName)s.', {roomName: name}) : _t('You are trying to access a room.') } { name ? _t('You are trying to access %(roomName)s.', {roomName: name}) : _t('You are trying to access a room.') }
<br /> <br />
{ _tJsx("<a>Click here</a> to join the discussion!", { _t("<a>Click here</a> to join the discussion!",
/<a>(.*?)<\/a>/, {},
(sub) => <a onClick={this.props.onJoinClick}><b>{ sub }</b></a>, { 'a': (sub) => <a onClick={this.props.onJoinClick}><b>{ sub }</b></a> },
) } ) }
</div> </div>
</div> </div>

View file

@ -17,14 +17,14 @@ limitations under the License.
import Promise from 'bluebird'; import Promise from 'bluebird';
import React from 'react'; import React from 'react';
import { _t, _tJsx, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index'; import sdk from '../../../index';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import ObjectUtils from '../../../ObjectUtils'; import ObjectUtils from '../../../ObjectUtils';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import UserSettingsStore from '../../../UserSettingsStore';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
// parse a string as an integer; if the input is undefined, or cannot be parsed // parse a string as an integer; if the input is undefined, or cannot be parsed
@ -311,7 +311,7 @@ module.exports = React.createClass({
// url preview settings // url preview settings
const ps = this.saveUrlPreviewSettings(); const ps = this.saveUrlPreviewSettings();
if (ps.length > 0) { if (ps.length > 0) {
promises.push(ps); ps.map((p) => promises.push(p));
} }
// related groups // related groups
@ -363,26 +363,16 @@ module.exports = React.createClass({
}, },
saveBlacklistUnverifiedDevicesPerRoom: function() { saveBlacklistUnverifiedDevicesPerRoom: function() {
if (!this.refs.blacklistUnverified) return; if (!this.refs.blacklistUnverifiedDevices) return;
if (this._isRoomBlacklistUnverified() !== this.refs.blacklistUnverified.checked) { this.refs.blacklistUnverifiedDevices.save().then(() => {
this._setRoomBlacklistUnverified(this.refs.blacklistUnverified.checked); const value = SettingsStore.getValueAt(
} SettingLevel.ROOM_DEVICE,
}, "blacklistUnverifiedDevices",
this.props.room.roomId,
_isRoomBlacklistUnverified: function() { /*explicit=*/true,
const blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom; );
if (blacklistUnverifiedDevicesPerRoom) {
return blacklistUnverifiedDevicesPerRoom[this.props.room.roomId];
}
return false;
},
_setRoomBlacklistUnverified: function(value) {
const blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom || {};
blacklistUnverifiedDevicesPerRoom[this.props.room.roomId] = value;
UserSettingsStore.setLocalSetting('blacklistUnverifiedDevicesPerRoom', blacklistUnverifiedDevicesPerRoom);
this.props.room.setBlacklistUnverifiedDevices(value); this.props.room.setBlacklistUnverifiedDevices(value);
});
}, },
_hasDiff: function(strA, strB) { _hasDiff: function(strA, strB) {
@ -588,19 +578,20 @@ module.exports = React.createClass({
}, },
_renderEncryptionSection: function() { _renderEncryptionSection: function() {
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const roomState = this.props.room.currentState; const roomState = this.props.room.currentState;
const isEncrypted = cli.isRoomEncrypted(this.props.room.roomId); const isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
const isGlobalBlacklistUnverified = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevices;
const isRoomBlacklistUnverified = this._isRoomBlacklistUnverified();
const settings = const settings = (
<label> <SettingsFlag name="blacklistUnverifiedDevices"
<input type="checkbox" ref="blacklistUnverified" level={SettingLevel.ROOM_DEVICE}
defaultChecked={isGlobalBlacklistUnverified || isRoomBlacklistUnverified} roomId={this.props.room.roomId}
disabled={isGlobalBlacklistUnverified || (this.refs.encrypt && !this.refs.encrypt.checked)} /> manualSave={true}
{ _t('Never send encrypted messages to unverified devices in this room from this device') }. ref="blacklistUnverifiedDevices"
</label>; />
);
if (!isEncrypted && roomState.mayClientSendStateEvent("m.room.encryption", cli)) { if (!isEncrypted && roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
return ( return (
@ -637,9 +628,7 @@ module.exports = React.createClass({
const ColorSettings = sdk.getComponent("room_settings.ColorSettings"); const ColorSettings = sdk.getComponent("room_settings.ColorSettings");
const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings"); const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings"); const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
const EditableText = sdk.getComponent('elements.EditableText');
const PowerSelector = sdk.getComponent('elements.PowerSelector'); const PowerSelector = sdk.getComponent('elements.PowerSelector');
const Loader = sdk.getComponent("elements.Spinner");
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const roomState = this.props.room.currentState; const roomState = this.props.room.currentState;
@ -758,9 +747,9 @@ module.exports = React.createClass({
} }
}); });
var tagsSection = null; let tagsSection = null;
if (canSetTag || self.state.tags) { if (canSetTag || self.state.tags) {
var tagsSection = tagsSection =
<div className="mx_RoomSettings_tags"> <div className="mx_RoomSettings_tags">
{ _t("Tagged as: ") }{ canSetTag ? { _t("Tagged as: ") }{ canSetTag ?
(tags.map(function(tag, i) { (tags.map(function(tag, i) {
@ -790,10 +779,10 @@ module.exports = React.createClass({
if (this.state.join_rule === "public" && aliasCount == 0) { if (this.state.join_rule === "public" && aliasCount == 0) {
addressWarning = addressWarning =
<div className="mx_RoomSettings_warning"> <div className="mx_RoomSettings_warning">
{ _tJsx( { _t(
'To link to a room it must have <a>an address</a>.', 'To link to a room it must have <a>an address</a>.',
/<a>(.*?)<\/a>/, {},
(sub) => <a href="#addresses">{ sub }</a>, { 'a': (sub) => <a href="#addresses">{ sub }</a> },
) } ) }
</div>; </div>;
} }
@ -910,41 +899,41 @@ module.exports = React.createClass({
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings"> <div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('The default role for new room members is') } </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('The default role for new room members is') } </span>
<PowerSelector ref="users_default" value={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < default_user_level} onChange={this.onPowerLevelsChanged} /> <PowerSelector ref="users_default" value={default_user_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < default_user_level} onChange={this.onPowerLevelsChanged} />
</div> </div>
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To send messages, you must be a') } </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To send messages, you must be a') } </span>
<PowerSelector ref="events_default" value={send_level} controlled={false} disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged} /> <PowerSelector ref="events_default" value={send_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged} />
</div> </div>
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To invite users into the room, you must be a') } </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To invite users into the room, you must be a') } </span>
<PowerSelector ref="invite" value={invite_level} controlled={false} disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged} /> <PowerSelector ref="invite" value={invite_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged} />
</div> </div>
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To configure the room, you must be a') } </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To configure the room, you must be a') } </span>
<PowerSelector ref="state_default" value={state_level} controlled={false} disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged} /> <PowerSelector ref="state_default" value={state_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged} />
</div> </div>
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To kick users, you must be a') } </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To kick users, you must be a') } </span>
<PowerSelector ref="kick" value={kick_level} controlled={false} disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged} /> <PowerSelector ref="kick" value={kick_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged} />
</div> </div>
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To ban users, you must be a') } </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To ban users, you must be a') } </span>
<PowerSelector ref="ban" value={ban_level} controlled={false} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged} /> <PowerSelector ref="ban" value={ban_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged} />
</div> </div>
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To remove other users\' messages, you must be a') } </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To remove other users\' messages, you must be a') } </span>
<PowerSelector ref="redact" value={redact_level} controlled={false} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged} /> <PowerSelector ref="redact" value={redact_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged} />
</div> </div>
{ Object.keys(events_levels).map(function(event_type, i) { { Object.keys(events_levels).map(function(event_type, i) {
let label = plEventsToLabels[event_type]; let label = plEventsToLabels[event_type];
if (label) label = _t(label); if (label) label = _t(label);
else label = _tJsx("To send events of type <eventType/>, you must be a", /<eventType\/>/, () => <code>{ event_type }</code>); else label = _t("To send events of type <eventType/>, you must be a", {}, { 'eventType': <code>{ event_type }</code> });
return ( return (
<div className="mx_RoomSettings_powerLevel" key={event_type}> <div className="mx_RoomSettings_powerLevel" key={event_type}>
<span className="mx_RoomSettings_powerLevelKey">{ label } </span> <span className="mx_RoomSettings_powerLevelKey">{ label } </span>
<PowerSelector ref={"event_levels_"+event_type} value={events_levels[event_type]} onChange={self.onPowerLevelsChanged} <PowerSelector ref={"event_levels_"+event_type} value={events_levels[event_type]} usersDefault={default_user_level} onChange={self.onPowerLevelsChanged}
controlled={false} disabled={!can_change_levels || current_user_level < events_levels[event_type]} /> controlled={false} disabled={!can_change_levels || current_user_level < events_levels[event_type]} />
</div> </div>
); );

View file

@ -27,7 +27,6 @@ const ContextualMenu = require('../../structures/ContextualMenu');
const RoomNotifs = require('../../../RoomNotifs'); const RoomNotifs = require('../../../RoomNotifs');
const FormattingUtils = require('../../../utils/FormattingUtils'); const FormattingUtils = require('../../../utils/FormattingUtils');
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
const UserSettingsStore = require('../../../UserSettingsStore');
import ActiveRoomObserver from '../../../ActiveRoomObserver'; import ActiveRoomObserver from '../../../ActiveRoomObserver';
import RoomViewStore from '../../../stores/RoomViewStore'; import RoomViewStore from '../../../stores/RoomViewStore';

View file

@ -20,7 +20,7 @@ import classNames from 'classnames';
import sdk from '../../../index'; import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
export default class DevicesPanel extends React.Component { export default class DevicesPanel extends React.Component {
constructor(props, context) { constructor(props, context) {
@ -29,11 +29,16 @@ export default class DevicesPanel extends React.Component {
this.state = { this.state = {
devices: undefined, devices: undefined,
deviceLoadError: undefined, deviceLoadError: undefined,
selectedDevices: [],
deleting: false,
}; };
this._unmounted = false; this._unmounted = false;
this._renderDevice = this._renderDevice.bind(this); this._renderDevice = this._renderDevice.bind(this);
this._onDeviceSelectionToggled = this._onDeviceSelectionToggled.bind(this);
this._onDeleteClick = this._onDeleteClick.bind(this);
} }
componentDidMount() { componentDidMount() {
@ -82,25 +87,78 @@ export default class DevicesPanel extends React.Component {
return (idA < idB) ? -1 : (idA > idB) ? 1 : 0; return (idA < idB) ? -1 : (idA > idB) ? 1 : 0;
} }
_onDeviceDeleted(device) { _onDeviceSelectionToggled(device) {
if (this._unmounted) { return; } if (this._unmounted) { return; }
// delete the removed device from our list. const deviceId = device.device_id;
const removed_id = device.device_id;
this.setState((state, props) => { this.setState((state, props) => {
const newDevices = state.devices.filter( // Make a copy of the selected devices, then add or remove the device
(d) => { return d.device_id != removed_id; }, const selectedDevices = state.selectedDevices.slice();
);
return { devices: newDevices }; const i = selectedDevices.indexOf(deviceId);
if (i === -1) {
selectedDevices.push(deviceId);
} else {
selectedDevices.splice(i, 1);
}
return {selectedDevices};
}); });
} }
_onDeleteClick() {
this.setState({
deleting: true,
});
this._makeDeleteRequest(null).catch((error) => {
if (this._unmounted) { return; }
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
// doesn't look like an interactive-auth failure
throw error;
}
// pop up an interactive auth dialog
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
Modal.createTrackedDialog('Delete Device Dialog', '', InteractiveAuthDialog, {
title: _t("Authentication"),
matrixClient: MatrixClientPeg.get(),
authData: error.data,
makeRequest: this._makeDeleteRequest.bind(this),
});
}).catch((e) => {
console.error("Error deleting devices", e);
if (this._unmounted) { return; }
}).finally(() => {
this.setState({
deleting: false,
});
});
}
_makeDeleteRequest(auth) {
return MatrixClientPeg.get().deleteMultipleDevices(this.state.selectedDevices, auth).then(
() => {
// Remove the deleted devices from `devices`, reset selection to []
this.setState({
devices: this.state.devices.filter(
(d) => !this.state.selectedDevices.includes(d.device_id),
),
selectedDevices: [],
});
},
);
}
_renderDevice(device) { _renderDevice(device) {
const DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry'); const DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry');
return ( return <DevicesPanelEntry
<DevicesPanelEntry key={device.device_id} device={device} key={device.device_id}
onDeleted={()=>{this._onDeviceDeleted(device);}} /> device={device}
); selected={this.state.selectedDevices.includes(device.device_id)}
onDeviceToggled={this._onDeviceSelectionToggled}
/>;
} }
render() { render() {
@ -124,6 +182,12 @@ export default class DevicesPanel extends React.Component {
devices.sort(this._deviceCompare); devices.sort(this._deviceCompare);
const deleteButton = this.state.deleting ?
<Spinner w={22} h={22} /> :
<div className="mx_textButton" onClick={this._onDeleteClick}>
{ _t("Delete %(count)s devices", {count: this.state.selectedDevices.length}) }
</div>;
const classes = classNames(this.props.className, "mx_DevicesPanel"); const classes = classNames(this.props.className, "mx_DevicesPanel");
return ( return (
<div className={classes}> <div className={classes}>
@ -131,7 +195,9 @@ export default class DevicesPanel extends React.Component {
<div className="mx_DevicesPanel_deviceId">{ _t("Device ID") }</div> <div className="mx_DevicesPanel_deviceId">{ _t("Device ID") }</div>
<div className="mx_DevicesPanel_deviceName">{ _t("Device Name") }</div> <div className="mx_DevicesPanel_deviceName">{ _t("Device Name") }</div>
<div className="mx_DevicesPanel_deviceLastSeen">{ _t("Last seen") }</div> <div className="mx_DevicesPanel_deviceLastSeen">{ _t("Last seen") }</div>
<div className="mx_DevicesPanel_deviceButtons"></div> <div className="mx_DevicesPanel_deviceButtons">
{ this.state.selectedDevices.length > 0 ? deleteButton : _t('Select devices') }
</div>
</div> </div>
{ devices.map(this._renderDevice) } { devices.map(this._renderDevice) }
</div> </div>

Some files were not shown because too many files have changed in this diff Show more