Fix conflict and update strings
Signed-off-by: Stefan Parviainen <pafcu@iki.fi>
This commit is contained in:
commit
2c1618bc10
70 changed files with 3229 additions and 1095 deletions
|
@ -52,13 +52,13 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import UserSettingsStore from './UserSettingsStore';
|
||||
import PlatformPeg from './PlatformPeg';
|
||||
import Modal from './Modal';
|
||||
import sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import dis from './dispatcher';
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
|
||||
global.mxCalls = {
|
||||
//room_id: MatrixCall
|
||||
|
@ -246,7 +246,7 @@ function _onAction(payload) {
|
|||
} else if (members.length === 2) {
|
||||
console.log("Place %s call in %s", payload.type, payload.room_id);
|
||||
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id, {
|
||||
forceTURN: UserSettingsStore.getLocalSetting('webRtcForceTURN', false),
|
||||
forceTURN: SettingsStore.getValue('webRtcForceTURN'),
|
||||
});
|
||||
placeCall(call);
|
||||
} else { // > 2
|
||||
|
|
|
@ -14,8 +14,8 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import UserSettingsStore from './UserSettingsStore';
|
||||
import * as Matrix from 'matrix-js-sdk';
|
||||
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
||||
|
||||
export default {
|
||||
getDevices: function() {
|
||||
|
@ -43,22 +43,20 @@ export default {
|
|||
},
|
||||
|
||||
loadDevices: function() {
|
||||
// this.getDevices().then((devices) => {
|
||||
const localSettings = UserSettingsStore.getLocalSettings();
|
||||
// // if deviceId is not found, automatic fallback is in spec
|
||||
// // recall previously stored inputs if any
|
||||
Matrix.setMatrixCallAudioInput(localSettings['webrtc_audioinput']);
|
||||
Matrix.setMatrixCallVideoInput(localSettings['webrtc_videoinput']);
|
||||
// });
|
||||
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
||||
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
||||
|
||||
Matrix.setMatrixCallAudioInput(audioDeviceId);
|
||||
Matrix.setMatrixCallVideoInput(videoDeviceId);
|
||||
},
|
||||
|
||||
setAudioInput: function(deviceId) {
|
||||
UserSettingsStore.setLocalSetting('webrtc_audioinput', deviceId);
|
||||
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
||||
Matrix.setMatrixCallAudioInput(deviceId);
|
||||
},
|
||||
|
||||
setVideoInput: function(deviceId) {
|
||||
UserSettingsStore.setLocalSetting('webrtc_videoinput', deviceId);
|
||||
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
|
||||
Matrix.setMatrixCallVideoInput(deviceId);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -436,6 +436,10 @@ function startMatrixClient() {
|
|||
DMRoomMap.makeShared().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'});
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -204,6 +204,12 @@ export default class Login {
|
|||
}
|
||||
throw originalLoginError;
|
||||
}).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 (
|
||||
error.httpStatus === 403 &&
|
||||
loginParams.identifier.type === 'm.id.user' &&
|
||||
|
@ -211,6 +217,7 @@ export default class Login {
|
|||
) {
|
||||
return tryLowercaseUsername(originalLoginError);
|
||||
}
|
||||
*/
|
||||
throw originalLoginError;
|
||||
}).catch((error) => {
|
||||
console.log("Login failed", error);
|
||||
|
|
|
@ -93,6 +93,7 @@ class MatrixClientPeg {
|
|||
const opts = utils.deepCopy(this.opts);
|
||||
// the react sdk doesn't work without this, so don't allow
|
||||
opts.pendingEventOrdering = "detached";
|
||||
opts.disablePresence = true; // we do this manually
|
||||
|
||||
try {
|
||||
const promise = this.matrixClient.store.startup();
|
||||
|
|
|
@ -25,6 +25,7 @@ import dis from './dispatcher';
|
|||
import sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import Modal from './Modal';
|
||||
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
||||
|
||||
/*
|
||||
* Dispatches:
|
||||
|
@ -138,10 +139,8 @@ const Notifier = {
|
|||
|
||||
// make sure that we persist the current setting audio_enabled setting
|
||||
// before changing anything
|
||||
if (global.localStorage) {
|
||||
if (global.localStorage.getItem('audio_notifications_enabled') === null) {
|
||||
this.setAudioEnabled(this.isEnabled());
|
||||
}
|
||||
if (SettingsStore.isLevelSupported(SettingLevel.DEVICE)) {
|
||||
SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, this.isEnabled());
|
||||
}
|
||||
|
||||
if (enable) {
|
||||
|
@ -149,6 +148,7 @@ const Notifier = {
|
|||
plaf.requestNotificationPermission().done((result) => {
|
||||
if (result !== 'granted') {
|
||||
// The permission request was dismissed or denied
|
||||
// TODO: Support alternative branding in messaging
|
||||
const description = result === 'denied'
|
||||
? _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');
|
||||
|
@ -160,10 +160,6 @@ const Notifier = {
|
|||
return;
|
||||
}
|
||||
|
||||
if (global.localStorage) {
|
||||
global.localStorage.setItem('notifications_enabled', 'true');
|
||||
}
|
||||
|
||||
if (callback) callback();
|
||||
dis.dispatch({
|
||||
action: "notifier_enabled",
|
||||
|
@ -174,8 +170,6 @@ const Notifier = {
|
|||
// disabled again in the future, we will show the banner again.
|
||||
this.setToolbarHidden(false);
|
||||
} else {
|
||||
if (!global.localStorage) return;
|
||||
global.localStorage.setItem('notifications_enabled', 'false');
|
||||
dis.dispatch({
|
||||
action: "notifier_enabled",
|
||||
value: false,
|
||||
|
@ -184,44 +178,24 @@ const Notifier = {
|
|||
},
|
||||
|
||||
isEnabled: function() {
|
||||
return this.isPossible() && SettingsStore.getValue("notificationsEnabled");
|
||||
},
|
||||
|
||||
isPossible: function() {
|
||||
const plaf = PlatformPeg.get();
|
||||
if (!plaf) return false;
|
||||
if (!plaf.supportsNotifications()) return false;
|
||||
if (!plaf.maySendNotifications()) return false;
|
||||
|
||||
if (!global.localStorage) return true;
|
||||
|
||||
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');
|
||||
return true; // possible, but not necessarily enabled
|
||||
},
|
||||
|
||||
isBodyEnabled: function() {
|
||||
if (!global.localStorage) return true;
|
||||
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';
|
||||
return this.isEnabled() && SettingsStore.getValue("notificationBodyEnabled");
|
||||
},
|
||||
|
||||
setAudioEnabled: function(enable) {
|
||||
if (!global.localStorage) return;
|
||||
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';
|
||||
isAudioEnabled: function() {
|
||||
return this.isEnabled() && SettingsStore.getValue("audioNotificationsEnabled");
|
||||
},
|
||||
|
||||
setToolbarHidden: function(hidden, persistent = true) {
|
||||
|
@ -238,16 +212,14 @@ const Notifier = {
|
|||
|
||||
// update the info to localStorage for persistent settings
|
||||
if (persistent && global.localStorage) {
|
||||
global.localStorage.setItem('notifications_hidden', hidden);
|
||||
global.localStorage.setItem("notifications_hidden", hidden);
|
||||
}
|
||||
},
|
||||
|
||||
isToolbarHidden: function() {
|
||||
// Check localStorage for any such meta data
|
||||
if (global.localStorage) {
|
||||
if (global.localStorage.getItem('notifications_hidden') === 'true') {
|
||||
return true;
|
||||
}
|
||||
return global.localStorage.getItem("notifications_hidden") === "true";
|
||||
}
|
||||
|
||||
return this.toolbarHidden;
|
||||
|
|
|
@ -56,13 +56,27 @@ class Presence {
|
|||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current status message.
|
||||
* @returns {String} the status message, may be null
|
||||
*/
|
||||
getStatusMessage() {
|
||||
return this.statusMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the presence state.
|
||||
* If the state has changed, the Home Server will be notified.
|
||||
* @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) {
|
||||
if (newState === this.state) {
|
||||
setState(newState, statusMessage=null, maintain=false) {
|
||||
if (this.maintain) {
|
||||
// Don't update presence if we're maintaining a particular status
|
||||
return;
|
||||
}
|
||||
if (newState === this.state && statusMessage === this.statusMessage) {
|
||||
return;
|
||||
}
|
||||
if (PRESENCE_STATES.indexOf(newState) === -1) {
|
||||
|
@ -72,21 +86,37 @@ class Presence {
|
|||
return;
|
||||
}
|
||||
const old_state = this.state;
|
||||
const old_message = this.statusMessage;
|
||||
this.state = newState;
|
||||
this.statusMessage = statusMessage;
|
||||
this.maintain = maintain;
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
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;
|
||||
MatrixClientPeg.get().setPresence(this.state).done(function() {
|
||||
MatrixClientPeg.get().setPresence(updateContent).done(function() {
|
||||
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) {
|
||||
console.error("Failed to set presence: %s", err);
|
||||
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.
|
||||
* @private
|
||||
|
@ -95,7 +125,8 @@ class Presence {
|
|||
this.setState("unavailable");
|
||||
}
|
||||
|
||||
_onUserActivity() {
|
||||
_onUserActivity(payload) {
|
||||
if (payload.action === "sync_state" || payload.action === "self_presence_updated") return;
|
||||
this._resetTimer();
|
||||
}
|
||||
|
||||
|
|
11
src/Roles.js
11
src/Roles.js
|
@ -15,19 +15,20 @@ limitations under the License.
|
|||
*/
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
export function levelRoleMap() {
|
||||
export function levelRoleMap(usersDefault) {
|
||||
return {
|
||||
undefined: _t('Default'),
|
||||
0: _t('User'),
|
||||
0: _t('Restricted'),
|
||||
[usersDefault]: _t('Default'),
|
||||
50: _t('Moderator'),
|
||||
100: _t('Admin'),
|
||||
};
|
||||
}
|
||||
|
||||
export function textualPowerLevel(level, userDefault) {
|
||||
const LEVEL_ROLE_MAP = this.levelRoleMap();
|
||||
export function textualPowerLevel(level, usersDefault) {
|
||||
const LEVEL_ROLE_MAP = this.levelRoleMap(usersDefault);
|
||||
if (LEVEL_ROLE_MAP[level]) {
|
||||
return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`);
|
||||
return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${usersDefault})`);
|
||||
} else {
|
||||
return level;
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ const DEFAULTS = {
|
|||
class SdkConfig {
|
||||
|
||||
static get() {
|
||||
return global.mxReactSdkConfig;
|
||||
return global.mxReactSdkConfig || {};
|
||||
}
|
||||
|
||||
static put(cfg) {
|
||||
|
|
|
@ -20,6 +20,7 @@ import Tinter from "./Tinter";
|
|||
import sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import Modal from './Modal';
|
||||
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
||||
|
||||
|
||||
class Command {
|
||||
|
@ -97,9 +98,7 @@ const commands = {
|
|||
colorScheme.secondary_color = matches[4];
|
||||
}
|
||||
return success(
|
||||
MatrixClientPeg.get().setRoomAccountData(
|
||||
roomId, "org.matrix.room.color_scheme", colorScheme,
|
||||
),
|
||||
SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
492
src/Tinter.js
492
src/Tinter.js
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -14,148 +15,125 @@ See the License for the specific language governing permissions and
|
|||
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;
|
||||
|
||||
// The colour keys to be replaced as referred to in CSS
|
||||
const keyRgb = [
|
||||
"rgb(118, 207, 166)", // Vector Green
|
||||
"rgb(234, 245, 240)", // Vector Light Green
|
||||
"rgb(211, 239, 225)", // BottomLeftMenu overlay (20% Vector Green)
|
||||
];
|
||||
// utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue]
|
||||
function colorToRgb(color) {
|
||||
if (!color) {
|
||||
return [0, 0, 0];
|
||||
}
|
||||
|
||||
// Some algebra workings for calculating the tint % of Vector Green & Light Green
|
||||
// x * 118 + (1 - x) * 255 = 234
|
||||
// x * 118 + 255 - 255 * x = 234
|
||||
// x * 118 - x * 255 = 234 - 255
|
||||
// (255 - 118) x = 255 - 234
|
||||
// x = (255 - 234) / (255 - 118) = 0.16
|
||||
|
||||
// The colour keys to be replaced as referred to in SVGs
|
||||
const keyHex = [
|
||||
"#76CFA6", // Vector Green
|
||||
"#EAF5F0", // Vector Light Green
|
||||
"#D3EFE1", // BottomLeftMenu overlay (20% Vector Green overlaid on Vector Light Green)
|
||||
"#FFFFFF", // white highlights of the SVGs (for switching to dark theme)
|
||||
];
|
||||
|
||||
// cache of our replacement colours
|
||||
// defaults to our keys.
|
||||
const colors = [
|
||||
keyHex[0],
|
||||
keyHex[1],
|
||||
keyHex[2],
|
||||
keyHex[3],
|
||||
];
|
||||
|
||||
const cssFixups = [
|
||||
// {
|
||||
// style: a style object that should be fixed up taken from a stylesheet
|
||||
// attr: name of the attribute to be clobbered, e.g. 'color'
|
||||
// index: ordinal of primary, secondary or tertiary
|
||||
// }
|
||||
];
|
||||
|
||||
// CSS attributes to be fixed up
|
||||
const cssAttrs = [
|
||||
"color",
|
||||
"backgroundColor",
|
||||
"borderColor",
|
||||
"borderTopColor",
|
||||
"borderBottomColor",
|
||||
"borderLeftColor",
|
||||
];
|
||||
|
||||
const svgAttrs = [
|
||||
"fill",
|
||||
"stroke",
|
||||
];
|
||||
|
||||
let cached = false;
|
||||
|
||||
function calcCssFixups() {
|
||||
if (DEBUG) console.log("calcSvgFixups start");
|
||||
for (let i = 0; i < document.styleSheets.length; i++) {
|
||||
const ss = document.styleSheets[i];
|
||||
if (!ss) continue; // well done safari >:(
|
||||
// Chromium apparently sometimes returns null here; unsure why.
|
||||
// see $14534907369972FRXBx:matrix.org in HQ
|
||||
// ...ah, it's because there's a third party extension like
|
||||
// privacybadger inserting its own stylesheet in there with a
|
||||
// resource:// URI or something which results in a XSS error.
|
||||
// See also #vector:matrix.org/$145357669685386ebCfr:matrix.org
|
||||
// ...except some browsers apparently return stylesheets without
|
||||
// hrefs, which we have no choice but ignore right now
|
||||
|
||||
// XXX seriously? we are hardcoding the name of vector's CSS file in
|
||||
// here?
|
||||
//
|
||||
// Why do we need to limit it to vector's CSS file anyway - if there
|
||||
// are other CSS files affecting the doc don't we want to apply the
|
||||
// same transformations to them?
|
||||
//
|
||||
// Iterating through the CSS looking for matches to hack on feels
|
||||
// pretty horrible anyway. And what if the application skin doesn't use
|
||||
// Vector Green as its primary color?
|
||||
|
||||
if (ss.href && !ss.href.match(/\/bundle.*\.css$/)) continue;
|
||||
|
||||
if (!ss.cssRules) continue;
|
||||
for (let j = 0; j < ss.cssRules.length; j++) {
|
||||
const rule = ss.cssRules[j];
|
||||
if (!rule.style) continue;
|
||||
for (let k = 0; k < cssAttrs.length; k++) {
|
||||
const attr = cssAttrs[k];
|
||||
for (let l = 0; l < keyRgb.length; l++) {
|
||||
if (rule.style[attr] === keyRgb[l]) {
|
||||
cssFixups.push({
|
||||
style: rule.style,
|
||||
attr: attr,
|
||||
index: l,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
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]),
|
||||
];
|
||||
}
|
||||
}
|
||||
if (DEBUG) console.log("calcSvgFixups end");
|
||||
return [0, 0, 0];
|
||||
}
|
||||
|
||||
function applyCssFixups() {
|
||||
if (DEBUG) console.log("applyCssFixups start");
|
||||
for (let i = 0; i < cssFixups.length; i++) {
|
||||
const cssFixup = cssFixups[i];
|
||||
cssFixup.style[cssFixup.attr] = colors[cssFixup.index];
|
||||
}
|
||||
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) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
// List of functions to call when the tint changes.
|
||||
const tintables = [];
|
||||
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(234, 245, 240)", // Vector Light 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
|
||||
// x * 118 + (1 - x) * 255 = 234
|
||||
// x * 118 + 255 - 255 * x = 234
|
||||
// x * 118 - x * 255 = 234 - 255
|
||||
// (255 - 118) x = 255 - 234
|
||||
// x = (255 - 234) / (255 - 118) = 0.16
|
||||
|
||||
// The colour keys to be replaced as referred to in SVGs
|
||||
this.keyHex = [
|
||||
"#76CFA6", // Vector Green
|
||||
"#EAF5F0", // 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)
|
||||
"#000000", // black lowlights of the SVGs (for switching to dark theme)
|
||||
];
|
||||
|
||||
// track the replacement colours actually being used
|
||||
// defaults to our keys.
|
||||
this.colors = [
|
||||
this.keyHex[0],
|
||||
this.keyHex[1],
|
||||
this.keyHex[2],
|
||||
this.keyHex[3],
|
||||
this.keyHex[4],
|
||||
];
|
||||
|
||||
// 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
|
||||
// attr: name of the attribute to be clobbered, e.g. 'color'
|
||||
// index: ordinal of primary, secondary or tertiary
|
||||
// },
|
||||
// }
|
||||
];
|
||||
|
||||
// CSS attributes to be fixed up
|
||||
this.cssAttrs = [
|
||||
"color",
|
||||
"backgroundColor",
|
||||
"borderColor",
|
||||
"borderTopColor",
|
||||
"borderBottomColor",
|
||||
"borderLeftColor",
|
||||
];
|
||||
|
||||
this.svgAttrs = [
|
||||
"fill",
|
||||
"stroke",
|
||||
];
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Register a callback to fire when the tint changes.
|
||||
* This is used to rewrite the tintable SVGs with the new tint.
|
||||
|
@ -167,79 +145,243 @@ module.exports = {
|
|||
*
|
||||
* @param {Function} tintable Function to call when the tint changes.
|
||||
*/
|
||||
registerTintable: function(tintable) {
|
||||
tintables.push(tintable);
|
||||
},
|
||||
registerTintable(tintable) {
|
||||
this.tintables.push(tintable);
|
||||
}
|
||||
|
||||
tint: function(primaryColor, secondaryColor, tertiaryColor) {
|
||||
if (!cached) {
|
||||
calcCssFixups();
|
||||
cached = true;
|
||||
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 = "#76CFA6"; // Vector green
|
||||
secondaryColor = "#EAF5F0"; // Vector light green
|
||||
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 = hexToRgb(primaryColor);
|
||||
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 = rgbToHex(rgb);
|
||||
secondaryColor = rgbToColor(rgb);
|
||||
}
|
||||
|
||||
if (!tertiaryColor) {
|
||||
const x = 0.19;
|
||||
const rgb1 = hexToRgb(primaryColor);
|
||||
const rgb2 = hexToRgb(secondaryColor);
|
||||
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 = rgbToHex(rgb1);
|
||||
tertiaryColor = rgbToColor(rgb1);
|
||||
}
|
||||
|
||||
if (colors[0] === primaryColor &&
|
||||
colors[1] === secondaryColor &&
|
||||
colors[2] === tertiaryColor) {
|
||||
if (this.forceTint == false &&
|
||||
this.colors[0] === primaryColor &&
|
||||
this.colors[1] === secondaryColor &&
|
||||
this.colors[2] === tertiaryColor) {
|
||||
return;
|
||||
}
|
||||
|
||||
colors[0] = primaryColor;
|
||||
colors[1] = secondaryColor;
|
||||
colors[2] = tertiaryColor;
|
||||
this.forceTint = false;
|
||||
|
||||
if (DEBUG) console.log("Tinter.tint");
|
||||
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.
|
||||
applyCssFixups();
|
||||
this.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) {
|
||||
this.tintables.forEach(function(tintable) {
|
||||
tintable();
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
tintSvgWhite(whiteColor) {
|
||||
this.currentTint[3] = whiteColor;
|
||||
|
||||
tintSvgWhite: function(whiteColor) {
|
||||
if (!whiteColor) {
|
||||
whiteColor = colors[3];
|
||||
whiteColor = this.colors[3];
|
||||
}
|
||||
if (colors[3] === whiteColor) {
|
||||
if (this.colors[3] === whiteColor) {
|
||||
return;
|
||||
}
|
||||
colors[3] = whiteColor;
|
||||
tintables.forEach(function(tintable) {
|
||||
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] = [];
|
||||
|
||||
for (let i = 0; i < document.styleSheets.length; i++) {
|
||||
const ss = document.styleSheets[i];
|
||||
if (!ss) continue; // well done safari >:(
|
||||
// Chromium apparently sometimes returns null here; unsure why.
|
||||
// see $14534907369972FRXBx:matrix.org in HQ
|
||||
// ...ah, it's because there's a third party extension like
|
||||
// privacybadger inserting its own stylesheet in there with a
|
||||
// resource:// URI or something which results in a XSS error.
|
||||
// See also #vector:matrix.org/$145357669685386ebCfr:matrix.org
|
||||
// ...except some browsers apparently return stylesheets without
|
||||
// hrefs, which we have no choice but ignore right now
|
||||
|
||||
// XXX seriously? we are hardcoding the name of vector's CSS file in
|
||||
// here?
|
||||
//
|
||||
// Why do we need to limit it to vector's CSS file anyway - if there
|
||||
// are other CSS files affecting the doc don't we want to apply the
|
||||
// same transformations to them?
|
||||
//
|
||||
// Iterating through the CSS looking for matches to hack on feels
|
||||
// pretty horrible anyway. And what if the application skin doesn't use
|
||||
// Vector Green as its primary color?
|
||||
// --richvdh
|
||||
|
||||
// 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 (DEBUG) console.debug("calcCssFixups checking " + ss.cssRules.length + " rules for " + ss.href);
|
||||
|
||||
for (let j = 0; j < ss.cssRules.length; j++) {
|
||||
const rule = ss.cssRules[j];
|
||||
if (!rule.style) continue;
|
||||
if (rule.selectorText && rule.selectorText.match(/#mx_theme/)) continue;
|
||||
for (let k = 0; k < this.cssAttrs.length; k++) {
|
||||
const attr = this.cssAttrs[k];
|
||||
for (let l = 0; l < this.keyRgb.length; l++) {
|
||||
if (rule.style[attr] === this.keyRgb[l]) {
|
||||
this.cssFixups[this.theme].push({
|
||||
style: rule.style,
|
||||
attr: attr,
|
||||
index: l,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log("calcCssFixups end (" +
|
||||
this.cssFixups[this.theme].length +
|
||||
" fixups)");
|
||||
}
|
||||
}
|
||||
|
||||
applyCssFixups() {
|
||||
if (DEBUG) {
|
||||
console.log("applyCssFixups start (" +
|
||||
this.cssFixups[this.theme].length +
|
||||
" 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");
|
||||
}
|
||||
|
||||
// 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)
|
||||
// keeping it here for now.
|
||||
calcSvgFixups: function(svgs) {
|
||||
calcSvgFixups(svgs) {
|
||||
// go through manually fixing up SVG colours.
|
||||
// we could do this by stylesheets, but keeping the stylesheets
|
||||
// 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);
|
||||
const fixups = [];
|
||||
for (let i = 0; i < svgs.length; i++) {
|
||||
var svgDoc;
|
||||
let svgDoc;
|
||||
try {
|
||||
svgDoc = svgs[i].contentDocument;
|
||||
} catch(e) {
|
||||
|
@ -259,16 +401,17 @@ module.exports = {
|
|||
if (e.stack) {
|
||||
msg += ' | stack: ' + e.stack;
|
||||
}
|
||||
console.error(e);
|
||||
console.error(msg);
|
||||
}
|
||||
if (!svgDoc) continue;
|
||||
const tags = svgDoc.getElementsByTagName("*");
|
||||
for (let j = 0; j < tags.length; j++) {
|
||||
const tag = tags[j];
|
||||
for (let k = 0; k < svgAttrs.length; k++) {
|
||||
const attr = svgAttrs[k];
|
||||
for (let l = 0; l < keyHex.length; l++) {
|
||||
if (tag.getAttribute(attr) && tag.getAttribute(attr).toUpperCase() === keyHex[l]) {
|
||||
for (let k = 0; k < this.svgAttrs.length; k++) {
|
||||
const attr = this.svgAttrs[k];
|
||||
for (let l = 0; l < this.keyHex.length; l++) {
|
||||
if (tag.getAttribute(attr) &&
|
||||
tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) {
|
||||
fixups.push({
|
||||
node: tag,
|
||||
attr: attr,
|
||||
|
@ -282,14 +425,19 @@ module.exports = {
|
|||
if (DEBUG) console.log("calcSvgFixups end");
|
||||
|
||||
return fixups;
|
||||
},
|
||||
}
|
||||
|
||||
applySvgFixups: function(fixups) {
|
||||
applySvgFixups(fixups) {
|
||||
if (DEBUG) console.log("applySvgFixups start for " + fixups);
|
||||
for (let i = 0; i < fixups.length; 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 (global.singletonTinter === undefined) {
|
||||
global.singletonTinter = new Tinter();
|
||||
}
|
||||
export default global.singletonTinter;
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
const MatrixClientPeg = require('./MatrixClientPeg');
|
||||
import UserSettingsStore from './UserSettingsStore';
|
||||
import shouldHideEvent from './shouldHideEvent';
|
||||
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,
|
||||
// but currently we just guess.
|
||||
|
||||
const syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||
// Loop through messages, starting with the most recent...
|
||||
for (let i = room.timeline.length - 1; i >= 0; --i) {
|
||||
const ev = room.timeline[i];
|
||||
|
@ -74,7 +72,7 @@ module.exports = {
|
|||
// that counts and we can stop looking because the user's read
|
||||
// this and everything before.
|
||||
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
|
||||
// the read marker, so this room is definitely unread.
|
||||
return true;
|
||||
|
|
|
@ -17,54 +17,11 @@ limitations under the License.
|
|||
|
||||
import Promise from 'bluebird';
|
||||
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.
|
||||
*/
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
id: 'feature_pinning',
|
||||
name: _td("Message Pinning"),
|
||||
},
|
||||
];
|
||||
|
||||
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() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli.getProfileInfo(cli.credentials.userId);
|
||||
|
@ -87,36 +44,6 @@ export default {
|
|||
// 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) {
|
||||
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
|
||||
});
|
||||
},
|
||||
|
||||
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);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -26,7 +26,7 @@ import {PillCompletion} from './Components';
|
|||
import type {SelectionRange, Completion} from './Autocompleter';
|
||||
import _uniq from 'lodash/uniq';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import UserSettingsStore from '../UserSettingsStore';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
|
||||
import EmojiData from '../stripped-emoji.json';
|
||||
|
||||
|
@ -96,7 +96,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ module.exports = {
|
|||
menuHeight: React.PropTypes.number,
|
||||
chevronOffset: React.PropTypes.number,
|
||||
menuColour: React.PropTypes.string,
|
||||
chevronFace: React.PropTypes.string, // top, bottom, left, right
|
||||
},
|
||||
|
||||
getOrCreateContainer: function() {
|
||||
|
@ -58,12 +59,30 @@ module.exports = {
|
|||
}
|
||||
};
|
||||
|
||||
const position = {
|
||||
top: props.top,
|
||||
};
|
||||
const position = {};
|
||||
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 = {};
|
||||
if (props.chevronOffset) {
|
||||
if (props.chevronFace) {
|
||||
chevronFace = props.chevronFace;
|
||||
}
|
||||
if (chevronFace === 'top' || chevronFace === 'bottom') {
|
||||
chevronOffset.left = props.chevronOffset;
|
||||
} else {
|
||||
chevronOffset.top = props.chevronOffset;
|
||||
}
|
||||
|
||||
|
@ -74,28 +93,27 @@ module.exports = {
|
|||
.mx_ContextualMenu_chevron_left:after {
|
||||
border-right-color: ${props.menuColour};
|
||||
}
|
||||
|
||||
.mx_ContextualMenu_chevron_right:after {
|
||||
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;
|
||||
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 chevron = <div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace}></div>;
|
||||
const className = 'mx_ContextualMenu_wrapper';
|
||||
|
||||
const menuClasses = classNames({
|
||||
'mx_ContextualMenu': true,
|
||||
'mx_ContextualMenu_left': props.left,
|
||||
'mx_ContextualMenu_right': !props.left,
|
||||
'mx_ContextualMenu_left': chevronFace === 'left',
|
||||
'mx_ContextualMenu_right': chevronFace === 'right',
|
||||
'mx_ContextualMenu_top': chevronFace === 'top',
|
||||
'mx_ContextualMenu_bottom': chevronFace === 'bottom',
|
||||
});
|
||||
|
||||
const menuStyle = {};
|
||||
|
|
|
@ -19,7 +19,6 @@ limitations under the License.
|
|||
import * as Matrix from 'matrix-js-sdk';
|
||||
import React from 'react';
|
||||
|
||||
import UserSettingsStore from '../../UserSettingsStore';
|
||||
import KeyCode from '../../KeyCode';
|
||||
import Notifier from '../../Notifier';
|
||||
import PageTypes from '../../PageTypes';
|
||||
|
@ -28,6 +27,7 @@ import sdk from '../../index';
|
|||
import dis from '../../dispatcher';
|
||||
import sessionStore from '../../stores/SessionStore';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
/**
|
||||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
||||
|
@ -74,7 +74,7 @@ export default React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
// use compact timeline view
|
||||
useCompactLayout: UserSettingsStore.getSyncedSetting('useCompactLayout'),
|
||||
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ import React from 'react';
|
|||
import Matrix from "matrix-js-sdk";
|
||||
|
||||
import Analytics from "../../Analytics";
|
||||
import UserSettingsStore from '../../UserSettingsStore';
|
||||
import MatrixClientPeg from "../../MatrixClientPeg";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
|
@ -44,6 +43,7 @@ import createRoom from "../../createRoom";
|
|||
import * as UDEHandler from '../../UnknownDeviceErrorHandler';
|
||||
import KeyRequestHandler from '../../KeyRequestHandler';
|
||||
import { _t, getCurrentLanguage } from '../../languageHandler';
|
||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
const VIEWS = {
|
||||
|
@ -224,7 +224,7 @@ module.exports = React.createClass({
|
|||
componentWillMount: function() {
|
||||
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
|
||||
this.firstSyncComplete = false;
|
||||
|
@ -287,6 +287,11 @@ module.exports = React.createClass({
|
|||
this._windowWidth = 10000;
|
||||
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() {
|
||||
|
@ -586,6 +591,9 @@ module.exports = React.createClass({
|
|||
this._onWillStartClient();
|
||||
});
|
||||
break;
|
||||
case 'client_started':
|
||||
this._onClientStarted();
|
||||
break;
|
||||
case 'new_version':
|
||||
this.onVersion(
|
||||
payload.currentVersion, payload.newVersion,
|
||||
|
@ -883,7 +891,7 @@ module.exports = React.createClass({
|
|||
*/
|
||||
_onSetTheme: function(theme) {
|
||||
if (!theme) {
|
||||
theme = 'light';
|
||||
theme = this.props.config.default_theme || 'light';
|
||||
}
|
||||
|
||||
// look for the stylesheet elements.
|
||||
|
@ -906,18 +914,49 @@ module.exports = React.createClass({
|
|||
// 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
|
||||
// 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;
|
||||
|
||||
if (theme === 'dark') {
|
||||
// abuse the tinter to change all the SVG's #fff to #2d2d2d
|
||||
// XXX: obviously this shouldn't be hardcoded here.
|
||||
Tinter.tintSvgWhite('#2d2d2d');
|
||||
} else {
|
||||
Tinter.tintSvgWhite('#ffffff');
|
||||
const switchTheme = function() {
|
||||
// we re-enable our theme here just in case we raced with another
|
||||
// theme set request as per https://github.com/vector-im/riot-web/issues/5601.
|
||||
// We could alternatively lock or similar to stop the race, but
|
||||
// this is probably good enough for now.
|
||||
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();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1088,6 +1127,34 @@ module.exports = React.createClass({
|
|||
cli.on("crypto.roomKeyRequestCancellation", (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);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import UserSettingsStore from '../../UserSettingsStore';
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import dis from "../../dispatcher";
|
||||
import sdk from '../../index';
|
||||
|
@ -110,8 +109,6 @@ module.exports = React.createClass({
|
|||
// Velocity requires
|
||||
this._readMarkerGhostNode = null;
|
||||
|
||||
this._syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||
|
||||
this._isMounted = true;
|
||||
},
|
||||
|
||||
|
@ -251,7 +248,7 @@ module.exports = React.createClass({
|
|||
// Always show highlighted event
|
||||
if (this.props.highlightedEventId === mxEv.getId()) return true;
|
||||
|
||||
return !shouldHideEvent(mxEv, this._syncedSettings);
|
||||
return !shouldHideEvent(mxEv);
|
||||
},
|
||||
|
||||
_getEventTiles: function() {
|
||||
|
|
|
@ -29,7 +29,6 @@ const classNames = require("classnames");
|
|||
const Matrix = require("matrix-js-sdk");
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
const UserSettingsStore = require('../../UserSettingsStore');
|
||||
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
const ContentMessages = require("../../ContentMessages");
|
||||
const Modal = require("../../Modal");
|
||||
|
@ -46,6 +45,7 @@ import KeyCode from '../../KeyCode';
|
|||
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function() {};
|
||||
|
@ -149,8 +149,6 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership);
|
||||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||
|
||||
this._syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||
|
||||
// Start listening for RoomViewStore updates
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._onRoomViewStoreUpdate(true);
|
||||
|
@ -542,7 +540,7 @@ module.exports = React.createClass({
|
|||
// update unread count when scrolled up
|
||||
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
|
||||
// no change
|
||||
} else if (!shouldHideEvent(ev, this._syncedSettings)) {
|
||||
} else if (!shouldHideEvent(ev)) {
|
||||
this.setState((state, props) => {
|
||||
return {numUnreadMessages: state.numUnreadMessages + 1};
|
||||
});
|
||||
|
@ -616,38 +614,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_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({
|
||||
showUrlPreview: !roomPreviewUrls.getContent().disable,
|
||||
});
|
||||
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,
|
||||
showUrlPreview: SettingsStore.getValue("urlPreviewsEnabled", room.roomId),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -666,12 +634,7 @@ module.exports = React.createClass({
|
|||
const room = this.state.room;
|
||||
if (!room) return;
|
||||
|
||||
const color_scheme_event = room.getAccountData("org.matrix.room.color_scheme");
|
||||
let color_scheme = {};
|
||||
if (color_scheme_event) {
|
||||
color_scheme = color_scheme_event.getContent();
|
||||
// XXX: we should validate the event
|
||||
}
|
||||
const color_scheme = SettingsStore.getValue("roomColor", room.room_id);
|
||||
console.log("Tinter.tint from updateTint");
|
||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
},
|
||||
|
@ -1775,7 +1738,7 @@ module.exports = React.createClass({
|
|||
const messagePanel = (
|
||||
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||
showReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
|
||||
showReadReceipts={!SettingsStore.getValue('hideReadReceipts')}
|
||||
manageReadReceipts={!this.state.isPeeking}
|
||||
manageReadMarkers={!this.state.isPeeking}
|
||||
hidden={hideMessagePanel}
|
||||
|
|
|
@ -15,6 +15,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require("react-dom");
|
||||
import Promise from 'bluebird';
|
||||
|
@ -30,7 +32,6 @@ const ObjectUtils = require('../../ObjectUtils');
|
|||
const Modal = require("../../Modal");
|
||||
const UserActivity = require("../../UserActivity");
|
||||
const KeyCode = require('../../KeyCode');
|
||||
import UserSettingsStore from '../../UserSettingsStore';
|
||||
|
||||
const PAGINATE_SIZE = 20;
|
||||
const INITIAL_SIZE = 20;
|
||||
|
@ -129,8 +130,6 @@ var TimelinePanel = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
const syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||
|
||||
return {
|
||||
events: [],
|
||||
timelineLoading: true, // track whether our room timeline is loading
|
||||
|
@ -175,10 +174,10 @@ var TimelinePanel = React.createClass({
|
|||
clientSyncState: MatrixClientPeg.get().getSyncState(),
|
||||
|
||||
// should the event tiles have twelve hour times
|
||||
isTwelveHour: syncedSettings.showTwelveHourTimestamps,
|
||||
isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"),
|
||||
|
||||
// always show timestamps on event tiles?
|
||||
alwaysShowTimestamps: syncedSettings.alwaysShowTimestamps,
|
||||
alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -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
|
||||
limitations under the License.
|
||||
*/
|
||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
const sdk = require('../../index');
|
||||
|
@ -56,133 +58,64 @@ const gHVersionLabel = function(repo, token='') {
|
|||
return <a target="_blank" rel="noopener" href={url}>{ token }</a>;
|
||||
};
|
||||
|
||||
// Enumerate some simple 'flip a bit' UI settings (if any).
|
||||
// 'id' gives the key name in the im.vector.web.settings account data event
|
||||
// 'label' is how we describe it in the UI.
|
||||
// Warning: Each "label" string below must be added to i18n/strings/en_EN.json,
|
||||
// since they will be translated when rendered.
|
||||
const SETTINGS_LABELS = [
|
||||
{
|
||||
id: 'autoplayGifsAndVideos',
|
||||
label: _td('Autoplay GIFs and videos'),
|
||||
},
|
||||
{
|
||||
id: 'hideReadReceipts',
|
||||
label: _td('Hide read receipts'),
|
||||
},
|
||||
{
|
||||
id: 'dontSendTypingNotifications',
|
||||
label: _td("Don't send typing notifications"),
|
||||
},
|
||||
{
|
||||
id: 'alwaysShowTimestamps',
|
||||
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',
|
||||
},
|
||||
*/
|
||||
// Enumerate some simple 'flip a bit' UI settings (if any). The strings provided here
|
||||
// must be settings defined in SettingsStore.
|
||||
const SIMPLE_SETTINGS = [
|
||||
{ id: "urlPreviewsEnabled" },
|
||||
{ id: "autoplayGifsAndVideos" },
|
||||
{ id: "hideReadReceipts" },
|
||||
{ id: "dontSendTypingNotifications" },
|
||||
{ id: "alwaysShowTimestamps" },
|
||||
{ id: "showTwelveHourTimestamps" },
|
||||
{ id: "hideJoinLeaves" },
|
||||
{ id: "hideAvatarChanges" },
|
||||
{ id: "hideDisplaynameChanges" },
|
||||
{ id: "useCompactLayout" },
|
||||
{ id: "hideRedactions" },
|
||||
{ id: "enableSyntaxHighlightLanguageDetection" },
|
||||
{ id: "MessageComposerInput.autoReplaceEmoji" },
|
||||
{ id: "MessageComposerInput.dontSuggestEmoji" },
|
||||
{ id: "Pill.shouldHidePillAvatar" },
|
||||
{ id: "TextualBody.disableBigEmoji" },
|
||||
{ id: "VideoView.flipVideoHorizontally" },
|
||||
];
|
||||
|
||||
const ANALYTICS_SETTINGS_LABELS = [
|
||||
// These settings must be defined in SettingsStore
|
||||
const ANALYTICS_SETTINGS = [
|
||||
{
|
||||
id: 'analyticsOptOut',
|
||||
label: _td('Opt out of analytics'),
|
||||
fn: function(checked) {
|
||||
Analytics[checked ? 'disable' : 'enable']();
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const WEBRTC_SETTINGS_LABELS = [
|
||||
{
|
||||
id: 'webRtcForceTURN',
|
||||
label: _td('Disable Peer-to-Peer for 1:1 calls'),
|
||||
},
|
||||
// These settings must be defined in SettingsStore
|
||||
const WEBRTC_SETTINGS = [
|
||||
{ id: 'webRtcForceTURN' },
|
||||
];
|
||||
|
||||
// Warning: Each "label" string below must be added to i18n/strings/en_EN.json,
|
||||
// since they will be translated when rendered.
|
||||
const CRYPTO_SETTINGS_LABELS = [
|
||||
// These settings must be defined in SettingsStore
|
||||
const CRYPTO_SETTINGS = [
|
||||
{
|
||||
id: 'blacklistUnverifiedDevices',
|
||||
label: _td('Never send encrypted messages to unverified devices from this device'),
|
||||
fn: function(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.
|
||||
// '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.
|
||||
// 'value' is the value for the theme setting
|
||||
//
|
||||
// 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.
|
||||
// But for now for expedience we just hardcode them here.
|
||||
const THEMES = [
|
||||
{
|
||||
id: 'theme',
|
||||
label: _td('Light theme'),
|
||||
value: 'light',
|
||||
},
|
||||
{
|
||||
id: 'theme',
|
||||
label: _td('Dark theme'),
|
||||
value: 'dark',
|
||||
},
|
||||
{ label: _td('Light theme'), value: 'light' },
|
||||
{ label: _td('Dark theme'), value: 'dark' },
|
||||
{ label: _td('Status.im theme'), value: 'status' },
|
||||
];
|
||||
|
||||
const IgnoredUser = React.createClass({
|
||||
|
@ -204,7 +137,7 @@ const IgnoredUser = React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<li>
|
||||
<AccessibleButton onClick={this._onUnignoreClick} className="mx_UserSettings_button mx_UserSettings_buttonSmall">
|
||||
<AccessibleButton onClick={this._onUnignoreClick} className="mx_textButton">
|
||||
{ _t("Unignore") }
|
||||
</AccessibleButton>
|
||||
{ this.props.userId }
|
||||
|
@ -281,14 +214,6 @@ module.exports = React.createClass({
|
|||
});
|
||||
this._refreshFromServer();
|
||||
|
||||
const syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||
if (!syncedSettings.theme) {
|
||||
syncedSettings.theme = 'light';
|
||||
}
|
||||
this._syncedSettings = syncedSettings;
|
||||
|
||||
this._localSettings = UserSettingsStore.getLocalSettings();
|
||||
|
||||
if (PlatformPeg.get().isElectron()) {
|
||||
const {ipcRenderer} = require('electron');
|
||||
|
||||
|
@ -359,8 +284,8 @@ module.exports = React.createClass({
|
|||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
mediaDevices,
|
||||
activeAudioInput: this._localSettings['webrtc_audioinput'],
|
||||
activeVideoInput: this._localSettings['webrtc_videoinput'],
|
||||
activeAudioInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audioinput'),
|
||||
activeVideoInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_videoinput'),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -492,10 +417,6 @@ module.exports = React.createClass({
|
|||
dis.dispatch({action: 'password_changed'});
|
||||
},
|
||||
|
||||
onEnableNotificationsChange: function(event) {
|
||||
UserSettingsStore.setEnableNotifications(event.target.checked);
|
||||
},
|
||||
|
||||
_onAddEmailEditFinished: function(value, shouldSubmit) {
|
||||
if (!shouldSubmit) return;
|
||||
this._addEmail();
|
||||
|
@ -692,7 +613,8 @@ module.exports = React.createClass({
|
|||
|
||||
onLanguageChange: function(newLang) {
|
||||
if(this.state.language !== newLang) {
|
||||
UserSettingsStore.setLocalSetting('language', newLang);
|
||||
// We intentionally promote this to the account level at this point
|
||||
SettingsStore.setValue("language", null, SettingLevel.ACCOUNT, newLang);
|
||||
this.setState({
|
||||
language: newLang,
|
||||
});
|
||||
|
@ -715,14 +637,13 @@ module.exports = React.createClass({
|
|||
// 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) =>
|
||||
UserSettingsStore.setLocalSetting('autocompleteDelay', + e.target.value);
|
||||
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
|
||||
return (
|
||||
<div>
|
||||
<h3>{ _t("User Interface") }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{ this._renderUrlPreviewSelector() }
|
||||
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) }
|
||||
{ THEMES.map( this._renderThemeSelector ) }
|
||||
{ SIMPLE_SETTINGS.map( this._renderAccountSetting ) }
|
||||
{ THEMES.map( this._renderThemeOption ) }
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
|
@ -730,7 +651,7 @@ module.exports = React.createClass({
|
|||
<td>
|
||||
<input
|
||||
type="number"
|
||||
defaultValue={UserSettingsStore.getLocalSetting('autocompleteDelay', 200)}
|
||||
defaultValue={SettingsStore.getValueAt(SettingLevel.DEVICE, "autocompleteDelay")}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</td>
|
||||
|
@ -743,69 +664,31 @@ module.exports = React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
_renderUrlPreviewSelector: function() {
|
||||
return <div className="mx_UserSettings_toggle">
|
||||
<input id="urlPreviewsDisabled"
|
||||
type="checkbox"
|
||||
defaultChecked={UserSettingsStore.getUrlPreviewsDisabled()}
|
||||
onChange={this._onPreviewsDisabledChanged}
|
||||
/>
|
||||
<label htmlFor="urlPreviewsDisabled">
|
||||
{ _t("Disable inline URL previews by default") }
|
||||
</label>
|
||||
</div>;
|
||||
_renderAccountSetting: function(setting) {
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
return (
|
||||
<div className="mx_UserSettings_toggle" key={setting.id}>
|
||||
<SettingsFlag name={setting.id}
|
||||
label={setting.label}
|
||||
level={SettingLevel.ACCOUNT}
|
||||
onChange={setting.fn} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_onPreviewsDisabledChanged: function(e) {
|
||||
UserSettingsStore.setUrlPreviewsDisabled(e.target.checked);
|
||||
},
|
||||
|
||||
_renderSyncedSetting: 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) => {
|
||||
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}
|
||||
/>
|
||||
<label htmlFor={setting.id}>
|
||||
{ _t(setting.label) }
|
||||
</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>;
|
||||
_renderThemeOption: function(setting) {
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
const onChange = (v) => dis.dispatch({action: 'set_theme', value: setting.value});
|
||||
return (
|
||||
<div className="mx_UserSettings_toggle" key={setting.id + '_' + setting.value}>
|
||||
<SettingsFlag name="theme"
|
||||
label={setting.label}
|
||||
level={SettingLevel.ACCOUNT}
|
||||
onChange={onChange}
|
||||
group="theme"
|
||||
value={setting.value} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_renderCryptoInfo: function() {
|
||||
|
@ -847,7 +730,7 @@ module.exports = React.createClass({
|
|||
{ importExportButtons }
|
||||
</div>
|
||||
<div className="mx_UserSettings_section">
|
||||
{ CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) }
|
||||
{ CRYPTO_SETTINGS.map( this._renderDeviceSetting ) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -873,24 +756,16 @@ module.exports = React.createClass({
|
|||
} else return (<div />);
|
||||
},
|
||||
|
||||
_renderLocalSetting: 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) => {
|
||||
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>;
|
||||
_renderDeviceSetting: function(setting) {
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
return (
|
||||
<div className="mx_UserSettings_toggle" key={setting.id}>
|
||||
<SettingsFlag name={setting.id}
|
||||
label={setting.label}
|
||||
level={SettingLevel.DEVICE}
|
||||
onChange={setting.fn} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_renderDevicesPanel: function() {
|
||||
|
@ -927,18 +802,18 @@ module.exports = React.createClass({
|
|||
<h3>{ _t('Analytics') }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{ _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>;
|
||||
},
|
||||
|
||||
_renderLabs: function() {
|
||||
const features = [];
|
||||
UserSettingsStore.getLabsFeatures().forEach((featureId) => {
|
||||
SettingsStore.getLabsFeatures().forEach((featureId) => {
|
||||
// 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) => {
|
||||
UserSettingsStore.setFeatureEnabled(featureId, e.target.checked);
|
||||
SettingsStore.setFeatureEnabled(featureId, e.target.checked);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
|
@ -948,10 +823,10 @@ module.exports = React.createClass({
|
|||
type="checkbox"
|
||||
id={featureId}
|
||||
name={featureId}
|
||||
defaultChecked={UserSettingsStore.isFeatureEnabled(featureId)}
|
||||
defaultChecked={SettingsStore.isFeatureEnabled(featureId)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label htmlFor={featureId}>{ UserSettingsStore.translatedNameForFeature(featureId) }</label>
|
||||
<label htmlFor={featureId}>{ SettingsStore.getDisplayName(featureId) }</label>
|
||||
</div>);
|
||||
});
|
||||
|
||||
|
@ -1044,6 +919,8 @@ module.exports = React.createClass({
|
|||
const settings = this.state.electron_settings;
|
||||
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>
|
||||
<h3>{ _t('Desktop specific') }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
|
@ -1166,7 +1043,7 @@ module.exports = React.createClass({
|
|||
return <div>
|
||||
<h3>{ _t('VoIP') }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{ WEBRTC_SETTINGS_LABELS.map(this._renderLocalSetting) }
|
||||
{ WEBRTC_SETTINGS.map(this._renderDeviceSetting) }
|
||||
{ this._renderWebRtcDeviceSettings() }
|
||||
</div>
|
||||
</div>;
|
||||
|
|
|
@ -17,13 +17,13 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
const sdk = require('../../../index');
|
||||
const Modal = require("../../../Modal");
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
import sdk from '../../../index';
|
||||
import Modal from "../../../Modal";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
|
||||
const PasswordReset = require("../../../PasswordReset");
|
||||
import PasswordReset from "../../../PasswordReset";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ForgotPassword',
|
||||
|
@ -154,6 +154,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const LoginPage = sdk.getComponent("login.LoginPage");
|
||||
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||
const LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||
|
@ -165,7 +166,7 @@ module.exports = React.createClass({
|
|||
resetPasswordJsx = <Spinner />;
|
||||
} else if (this.state.progress === "sent_email") {
|
||||
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 }) }
|
||||
<br />
|
||||
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
||||
|
@ -174,7 +175,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
} else if (this.state.progress === "complete") {
|
||||
resetPasswordJsx = (
|
||||
<div>
|
||||
<div className="mx_Login_prompt">
|
||||
<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>
|
||||
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
||||
|
@ -182,6 +183,20 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
} 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 = (
|
||||
<div>
|
||||
<div className="mx_Login_prompt">
|
||||
|
@ -209,16 +224,7 @@ module.exports = React.createClass({
|
|||
<br />
|
||||
<input className="mx_Login_submit" type="submit" value={_t('Send Reset Email')} />
|
||||
</form>
|
||||
<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} />
|
||||
<div className="mx_Login_error">
|
||||
</div>
|
||||
{ serverConfigSection }
|
||||
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
||||
{ _t('Return to login screen') }
|
||||
</a>
|
||||
|
@ -233,12 +239,12 @@ module.exports = React.createClass({
|
|||
|
||||
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
<LoginPage>
|
||||
<div className="mx_Login_box">
|
||||
<LoginHeader />
|
||||
{ resetPasswordJsx }
|
||||
</div>
|
||||
</div>
|
||||
</LoginPage>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -22,8 +22,9 @@ import { _t } from '../../../languageHandler';
|
|||
import * as languageHandler from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import Login from '../../../Login';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
|
||||
|
@ -105,7 +106,22 @@ module.exports = React.createClass({
|
|||
if (error.httpStatus == 400 && usingEmail) {
|
||||
errorText = _t('This Home Server does not support login using email address.');
|
||||
} else if (error.httpStatus === 401 || error.httpStatus === 403) {
|
||||
errorText = _t('Incorrect username and/or password.');
|
||||
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.');
|
||||
}
|
||||
} else {
|
||||
// other errors, not specific to doing a password login
|
||||
errorText = this._errorTextFromError(error);
|
||||
|
@ -317,7 +333,7 @@ module.exports = React.createClass({
|
|||
|
||||
_onLanguageChange: function(newLang) {
|
||||
if(languageHandler.getCurrentLanguage() !== newLang) {
|
||||
UserSettingsStore.setLocalSetting('language', newLang);
|
||||
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
|
||||
PlatformPeg.get().reload();
|
||||
}
|
||||
},
|
||||
|
@ -334,6 +350,7 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const LoginPage = sdk.getComponent("login.LoginPage");
|
||||
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||
const LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||
|
@ -348,43 +365,69 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
let returnToAppJsx;
|
||||
/*
|
||||
// with the advent of ILAG I don't think we need this any more
|
||||
if (this.props.onCancelClick) {
|
||||
returnToAppJsx =
|
||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||
{ _t('Return to app') }
|
||||
</a>;
|
||||
}
|
||||
*/
|
||||
|
||||
let serverConfig;
|
||||
let header;
|
||||
|
||||
if (!SdkConfig.get().disable_custom_urls) {
|
||||
serverConfig = <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} />;
|
||||
}
|
||||
|
||||
// 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">
|
||||
{ this.state.errorText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
<LoginPage>
|
||||
<div className="mx_Login_box">
|
||||
<LoginHeader />
|
||||
<div>
|
||||
<h2>{ _t('Sign in') }
|
||||
{ loader }
|
||||
</h2>
|
||||
{ header }
|
||||
{ errorTextSection }
|
||||
{ this.componentForStep(this.state.currentFlow) }
|
||||
<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 className="mx_Login_error">
|
||||
{ this.state.errorText }
|
||||
</div>
|
||||
{ serverConfig }
|
||||
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||
{ _t('Create an account') }
|
||||
</a>
|
||||
{ loginAsGuestJsx }
|
||||
{ returnToAppJsx }
|
||||
{ this._renderLanguageSetting() }
|
||||
{ !SdkConfig.get().disable_login_language_selector ? this._renderLanguageSetting() : '' }
|
||||
<LoginFooter />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LoginPage>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -59,9 +59,10 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
|
||||
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||
const LoginPage = sdk.getComponent('login.LoginPage');
|
||||
const LoginHeader = sdk.getComponent('login.LoginHeader');
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
<LoginPage>
|
||||
<div className="mx_Login_box">
|
||||
<LoginHeader />
|
||||
<div className="mx_Login_profile">
|
||||
|
@ -74,7 +75,7 @@ module.exports = React.createClass({
|
|||
{ this.state.errorString }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LoginPage>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -26,6 +26,8 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
|||
import RegistrationForm from '../../views/login/RegistrationForm';
|
||||
import RtsClient from '../../../RtsClient';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const MIN_PASSWORD_LENGTH = 6;
|
||||
|
||||
|
@ -302,7 +304,7 @@ module.exports = React.createClass({
|
|||
} : {};
|
||||
|
||||
return this._matrixClient.register(
|
||||
this.state.formVals.username.toLowerCase(),
|
||||
this.state.formVals.username,
|
||||
this.state.formVals.password,
|
||||
undefined, // session id: included in the auth dict already
|
||||
auth,
|
||||
|
@ -322,10 +324,13 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
const LoginHeader = sdk.getComponent('login.LoginHeader');
|
||||
const LoginFooter = sdk.getComponent('login.LoginFooter');
|
||||
const LoginPage = sdk.getComponent('login.LoginPage');
|
||||
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
const ServerConfig = sdk.getComponent('views.login.ServerConfig');
|
||||
|
||||
const theme = SettingsStore.getValue("theme");
|
||||
|
||||
let registerBody;
|
||||
if (this.state.doingUIAuth) {
|
||||
registerBody = (
|
||||
|
@ -344,9 +349,19 @@ module.exports = React.createClass({
|
|||
} else if (this.state.busy || this.state.teamServerBusy) {
|
||||
registerBody = <Spinner />;
|
||||
} else {
|
||||
let errorSection;
|
||||
if (this.state.errorText) {
|
||||
errorSection = <div className="mx_Login_error">{ this.state.errorText }</div>;
|
||||
let serverConfigSection;
|
||||
if (!SdkConfig.get().disable_custom_urls) {
|
||||
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 = (
|
||||
<div>
|
||||
|
@ -362,21 +377,14 @@ module.exports = React.createClass({
|
|||
onRegisterClick={this.onFormSubmit}
|
||||
onTeamSelected={this.onTeamSelected}
|
||||
/>
|
||||
{ errorSection }
|
||||
<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}
|
||||
/>
|
||||
{ serverConfigSection }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let returnToAppJsx;
|
||||
/*
|
||||
// with the advent of ILAG I don't think we need this any more
|
||||
if (this.props.onCancelClick) {
|
||||
returnToAppJsx = (
|
||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||
|
@ -384,8 +392,32 @@ module.exports = React.createClass({
|
|||
</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 (
|
||||
<div className="mx_Login">
|
||||
<LoginPage>
|
||||
<div className="mx_Login_box">
|
||||
<LoginHeader
|
||||
icon={this.state.teamSelected ?
|
||||
|
@ -393,15 +425,14 @@ module.exports = React.createClass({
|
|||
this.state.teamSelected.domain + "/icon.png" :
|
||||
null}
|
||||
/>
|
||||
<h2>{ _t('Create an account') }</h2>
|
||||
{ header }
|
||||
{ registerBody }
|
||||
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
||||
{ _t('I already have an account') }
|
||||
</a>
|
||||
{ signIn }
|
||||
{ errorText }
|
||||
{ returnToAppJsx }
|
||||
<LoginFooter />
|
||||
</div>
|
||||
</div>
|
||||
</LoginPage>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
157
src/components/views/avatars/MemberPresenceAvatar.js
Normal file
157
src/components/views/avatars/MemberPresenceAvatar.js
Normal file
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
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";
|
||||
|
||||
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() {
|
||||
const presenceState = this.props.member.user.presence;
|
||||
const 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();
|
||||
// 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
|
||||
if (!SettingsStore.isFeatureEnabled("feature_presence_management")) {
|
||||
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;
|
||||
},
|
||||
});
|
|
@ -20,6 +20,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
|||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import Resend from '../../../Resend';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
function DeviceListEntry(props) {
|
||||
const {userId, device} = props;
|
||||
|
@ -112,12 +113,13 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const client = MatrixClientPeg.get();
|
||||
const blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() ||
|
||||
this.props.room.getBlacklistUnverifiedDevices();
|
||||
if (this.state.devices === null) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
let warning;
|
||||
if (blacklistUnverified) {
|
||||
if (SettingsStore.getValue("blacklistUnverifiedDevices", this.props.room.roomId)) {
|
||||
warning = (
|
||||
<h4>
|
||||
{ _t("You are currently blacklisting unverified devices; to send " +
|
||||
|
|
|
@ -22,6 +22,7 @@ import React from 'react';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import TintableSvgButton from './TintableSvgButton';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
|
@ -371,8 +372,8 @@ export default React.createClass({
|
|||
// editing is done in scalar
|
||||
const showEditButton = Boolean(this._scalarClient && this._canUserModify());
|
||||
const deleteWidgetLabel = this._deleteWidgetLabel();
|
||||
let deleteIcon = 'img/cancel.svg';
|
||||
let deleteClasses = 'mx_filterFlipColor mx_AppTileMenuBarWidget';
|
||||
let deleteIcon = 'img/cancel_green.svg';
|
||||
let deleteClasses = 'mx_AppTileMenuBarWidget';
|
||||
if(this._canUserModify()) {
|
||||
deleteIcon = 'img/icon-delete-pink.svg';
|
||||
deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
|
||||
|
@ -381,25 +382,26 @@ export default React.createClass({
|
|||
return (
|
||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||
{ this.formatAppTileName() }
|
||||
<b>{ this.formatAppTileName() }</b>
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{ /* Edit widget */ }
|
||||
{ showEditButton && <img
|
||||
src="img/edit.svg"
|
||||
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
width="8" height="8"
|
||||
alt={_t('Edit')}
|
||||
{ showEditButton && <TintableSvgButton
|
||||
src="img/edit_green.svg"
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
title={_t('Edit')}
|
||||
onClick={this._onEditClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
|
||||
{ /* Delete widget */ }
|
||||
<img src={deleteIcon}
|
||||
className={deleteClasses}
|
||||
width="8" height="8"
|
||||
alt={_t(deleteWidgetLabel)}
|
||||
title={_t(deleteWidgetLabel)}
|
||||
onClick={this._onDeleteClick}
|
||||
<TintableSvgButton
|
||||
src={deleteIcon}
|
||||
className={deleteClasses}
|
||||
title={_t(deleteWidgetLabel)}
|
||||
onClick={this._onDeleteClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -18,8 +18,8 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import * as languageHandler from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
function languageMatchesSearchQuery(query, language) {
|
||||
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
|
||||
// country selected, but our parent component
|
||||
// doesn't know this, therefore we do this.
|
||||
const _localSettings = UserSettingsStore.getLocalSettings();
|
||||
if (_localSettings.hasOwnProperty('language')) {
|
||||
this.props.onOptionChange(_localSettings.language);
|
||||
const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||
if (language) {
|
||||
this.props.onOptionChange(language);
|
||||
}else {
|
||||
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
|
||||
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
|
||||
// values between mounting and the initial value propgating
|
||||
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||
let value = null;
|
||||
const _localSettings = UserSettingsStore.getLocalSettings();
|
||||
if (_localSettings.hasOwnProperty('language')) {
|
||||
value = this.props.value || _localSettings.language;
|
||||
if (language) {
|
||||
value = this.props.value || language;
|
||||
} else {
|
||||
const language = navigator.language || navigator.userLanguage;
|
||||
language = navigator.language || navigator.userLanguage;
|
||||
value = this.props.value || language;
|
||||
}
|
||||
|
||||
|
|
|
@ -20,14 +20,16 @@ import React from 'react';
|
|||
import * as Roles from '../../../Roles';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
let LEVEL_ROLE_MAP = {};
|
||||
const reverseRoles = {};
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'PowerSelector',
|
||||
|
||||
propTypes: {
|
||||
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
|
||||
// to reflect the current value, rather than left freeform.
|
||||
|
@ -43,78 +45,98 @@ module.exports = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
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() {
|
||||
LEVEL_ROLE_MAP = Roles.levelRoleMap();
|
||||
Object.keys(LEVEL_ROLE_MAP).forEach(function(key) {
|
||||
reverseRoles[LEVEL_ROLE_MAP[key]] = key;
|
||||
});
|
||||
this._initStateFromProps(this.props);
|
||||
},
|
||||
|
||||
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) {
|
||||
this.setState({ custom: event.target.value === "Custom" });
|
||||
if (event.target.value !== "Custom") {
|
||||
this.props.onChange(this.getValue());
|
||||
this.setState({ custom: event.target.value === "SELECT_VALUE_CUSTOM" });
|
||||
if (event.target.value !== "SELECT_VALUE_CUSTOM") {
|
||||
this.props.onChange(event.target.value);
|
||||
}
|
||||
},
|
||||
|
||||
onCustomBlur: function(event) {
|
||||
this.props.onChange(this.getValue());
|
||||
this.props.onChange(parseInt(this.refs.custom.value));
|
||||
},
|
||||
|
||||
onCustomKeyDown: function(event) {
|
||||
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() {
|
||||
let customPicker;
|
||||
if (this.state.custom) {
|
||||
let input;
|
||||
if (this.props.disabled) {
|
||||
input = <span>{ this.props.value }</span>;
|
||||
customPicker = <span>{ _t(
|
||||
"Custom of %(powerLevel)s",
|
||||
{ powerLevel: this.props.value },
|
||||
) }</span>;
|
||||
} 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;
|
||||
if (this.state.custom) {
|
||||
selectValue = "Custom";
|
||||
selectValue = "SELECT_VALUE_CUSTOM";
|
||||
} else {
|
||||
selectValue = LEVEL_ROLE_MAP[this.props.value] || "Custom";
|
||||
selectValue = this.state.levelRoleMap[this.props.value] ?
|
||||
this.props.value : "SELECT_VALUE_CUSTOM";
|
||||
}
|
||||
let select;
|
||||
if (this.props.disabled) {
|
||||
select = <span>{ selectValue }</span>;
|
||||
select = <span>{ this.state.levelRoleMap[selectValue] }</span>;
|
||||
} else {
|
||||
// Each level must have a definition in LEVEL_ROLE_MAP
|
||||
const levels = [0, 50, 100];
|
||||
let options = levels.map((level) => {
|
||||
// Each level must have a definition in this.state.levelRoleMap
|
||||
let options = this.state.options.map((level) => {
|
||||
return {
|
||||
value: LEVEL_ROLE_MAP[level],
|
||||
// Give a userDefault (users_default in the power event) of 0 but
|
||||
// because level !== undefined, this should never be used.
|
||||
text: Roles.textualPowerLevel(level, 0),
|
||||
value: level,
|
||||
text: Roles.textualPowerLevel(level, this.props.usersDefault),
|
||||
};
|
||||
});
|
||||
options.push({ value: "Custom", text: _t("Custom level") });
|
||||
options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") });
|
||||
options = options.map((op) => {
|
||||
return <option value={op.value} key={op.value}>{ op.text }</option>;
|
||||
});
|
||||
|
|
110
src/components/views/elements/SettingsFlag.js
Normal file
110
src/components/views/elements/SettingsFlag.js
Normal 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>
|
||||
);
|
||||
},
|
||||
});
|
61
src/components/views/elements/TintableSvgButton.js
Normal file
61
src/components/views/elements/TintableSvgButton.js
Normal 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() {},
|
||||
};
|
59
src/components/views/login/LoginPage.js
Normal file
59
src/components/views/login/LoginPage.js
Normal 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>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
|
@ -20,7 +20,7 @@ import classNames from 'classnames';
|
|||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {field_input_incorrect} from '../../../UiEffects';
|
||||
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
|
||||
/**
|
||||
* A pure UI component which displays a username/password form.
|
||||
|
@ -144,7 +144,10 @@ class PasswordLogin extends React.Component {
|
|||
type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
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}
|
||||
autoFocus
|
||||
disabled={disabled}
|
||||
|
@ -210,9 +213,9 @@ class PasswordLogin extends React.Component {
|
|||
|
||||
const loginField = this.renderLoginField(this.state.loginType, matrixIdText === '');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
let loginType;
|
||||
if (!SdkConfig.get().disable_3pid_login) {
|
||||
loginType = (
|
||||
<div className="mx_Login_type_container">
|
||||
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
|
||||
<Dropdown
|
||||
|
@ -225,6 +228,13 @@ class PasswordLogin extends React.Component {
|
|||
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
{ loginType }
|
||||
{ loginField }
|
||||
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
|
||||
name="password"
|
||||
|
|
|
@ -22,6 +22,8 @@ import Email from '../../../email';
|
|||
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const FIELD_EMAIL = 'field_email';
|
||||
const FIELD_PHONE_COUNTRY = 'field_phone_country';
|
||||
|
@ -122,7 +124,7 @@ module.exports = React.createClass({
|
|||
password: this.refs.password.value.trim(),
|
||||
email: email,
|
||||
phoneCountry: this.state.phoneCountry,
|
||||
phoneNumber: this.refs.phoneNumber.value.trim(),
|
||||
phoneNumber: this.refs.phoneNumber ? this.refs.phoneNumber.value.trim() : '',
|
||||
});
|
||||
|
||||
if (promise) {
|
||||
|
@ -180,7 +182,7 @@ module.exports = React.createClass({
|
|||
this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
|
||||
break;
|
||||
case FIELD_PHONE_NUMBER:
|
||||
const phoneNumber = this.refs.phoneNumber.value;
|
||||
const phoneNumber = this.refs.phoneNumber ? this.refs.phoneNumber.value : '';
|
||||
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
|
||||
this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
|
||||
break;
|
||||
|
@ -273,10 +275,14 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
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 = (
|
||||
<div>
|
||||
<input type="text" ref="email"
|
||||
autoFocus={true} placeholder={_t("Email address (optional)")}
|
||||
autoFocus={true} placeholder={ emailPlaceholder }
|
||||
defaultValue={this.props.defaultEmail}
|
||||
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
||||
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
||||
|
@ -306,28 +312,31 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||
const phoneSection = (
|
||||
<div className="mx_Login_phoneSection">
|
||||
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
|
||||
className="mx_Login_phoneCountry mx_Login_field_prefix"
|
||||
value={this.state.phoneCountry}
|
||||
isSmall={true}
|
||||
showPrefix={true}
|
||||
/>
|
||||
<input type="text" ref="phoneNumber"
|
||||
placeholder={_t("Mobile phone number (optional)")}
|
||||
defaultValue={this.props.defaultPhoneNumber}
|
||||
className={this._classForField(
|
||||
FIELD_PHONE_NUMBER,
|
||||
'mx_Login_phoneNumberField',
|
||||
'mx_Login_field',
|
||||
'mx_Login_field_has_prefix',
|
||||
)}
|
||||
onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}}
|
||||
value={self.state.phoneNumber}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
let phoneSection;
|
||||
if (!SdkConfig.get().disable_3pid_login) {
|
||||
phoneSection = (
|
||||
<div className="mx_Login_phoneSection">
|
||||
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
|
||||
className="mx_Login_phoneCountry mx_Login_field_prefix"
|
||||
value={this.state.phoneCountry}
|
||||
isSmall={true}
|
||||
showPrefix={true}
|
||||
/>
|
||||
<input type="text" ref="phoneNumber"
|
||||
placeholder={_t("Mobile phone number (optional)")}
|
||||
defaultValue={this.props.defaultPhoneNumber}
|
||||
className={this._classForField(
|
||||
FIELD_PHONE_NUMBER,
|
||||
'mx_Login_phoneNumberField',
|
||||
'mx_Login_field',
|
||||
'mx_Login_field_has_prefix',
|
||||
)}
|
||||
onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}}
|
||||
value={self.state.phoneNumber}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const registerButton = (
|
||||
<input className="mx_Login_submit" type="submit" value={_t("Register")} />
|
||||
|
|
|
@ -25,8 +25,8 @@ import sdk from '../../../index';
|
|||
import dis from '../../../dispatcher';
|
||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||
import Promise from 'bluebird';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MImageBody',
|
||||
|
@ -81,7 +81,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onImageEnter: function(e) {
|
||||
if (!this._isGif() || UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
|
||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
return;
|
||||
}
|
||||
const imgElement = e.target;
|
||||
|
@ -89,7 +89,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onImageLeave: function(e) {
|
||||
if (!this._isGif() || UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
|
||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
return;
|
||||
}
|
||||
const imgElement = e.target;
|
||||
|
@ -218,7 +218,7 @@ module.exports = React.createClass({
|
|||
|
||||
const contentUrl = this._getContentUrl();
|
||||
let thumbUrl;
|
||||
if (this._isGif() && UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
|
||||
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
thumbUrl = contentUrl;
|
||||
} else {
|
||||
thumbUrl = this._getThumbUrl();
|
||||
|
|
|
@ -21,8 +21,8 @@ import MFileBody from './MFileBody';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||
import Promise from 'bluebird';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MVideoBody',
|
||||
|
@ -151,7 +151,7 @@ module.exports = React.createClass({
|
|||
|
||||
const contentUrl = this._getContentUrl();
|
||||
const thumbUrl = this._getThumbUrl();
|
||||
const autoplay = UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false);
|
||||
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos");
|
||||
let height = null;
|
||||
let width = null;
|
||||
let poster = null;
|
||||
|
|
|
@ -29,11 +29,11 @@ import Modal from '../../../Modal';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import UserSettingsStore from "../../../UserSettingsStore";
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import ContextualMenu from '../../structures/ContextualMenu';
|
||||
import {RoomMember} from 'matrix-js-sdk';
|
||||
import classNames from 'classnames';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
@ -104,7 +104,7 @@ module.exports = React.createClass({
|
|||
setTimeout(() => {
|
||||
if (this._unmounted) return;
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
if (UserSettingsStore.getSyncedSetting("enableSyntaxHighlightLanguageDetection", false)) {
|
||||
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
|
||||
highlight.highlightBlock(blocks[i]);
|
||||
} else {
|
||||
// Only syntax highlight if there's a class starting with language-
|
||||
|
@ -169,7 +169,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
pillifyLinks: function(nodes) {
|
||||
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
|
||||
const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
|
||||
let node = nodes[0];
|
||||
while (node) {
|
||||
let pillified = false;
|
||||
|
@ -419,7 +419,7 @@ module.exports = React.createClass({
|
|||
const content = mxEvent.getContent();
|
||||
|
||||
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
|
||||
disableBigEmoji: UserSettingsStore.getSyncedSetting('TextualBody.disableBigEmoji', false),
|
||||
disableBigEmoji: SettingsStore.getValue('TextualBody.disableBigEmoji'),
|
||||
});
|
||||
|
||||
if (this.props.highlightLink) {
|
||||
|
|
|
@ -22,10 +22,11 @@ const MatrixClientPeg = require("../../../MatrixClientPeg");
|
|||
const Modal = require("../../../Modal");
|
||||
|
||||
import dis from '../../../dispatcher';
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
|
||||
const ROOM_COLORS = [
|
||||
// magic room default values courtesy of Ribot
|
||||
["#76cfa6", "#eaf5f0"],
|
||||
[Tinter.getKeyRgb()[0], Tinter.getKeyRgb()[1]],
|
||||
["#81bddb", "#eaf1f4"],
|
||||
["#bd79cb", "#f3eaf5"],
|
||||
["#c65d94", "#f5eaef"],
|
||||
|
@ -47,17 +48,17 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
const data = {
|
||||
index: 0,
|
||||
primary_color: ROOM_COLORS[0].primary_color,
|
||||
secondary_color: ROOM_COLORS[0].secondary_color,
|
||||
primary_color: ROOM_COLORS[0][0],
|
||||
secondary_color: ROOM_COLORS[0][1],
|
||||
hasChanged: false,
|
||||
};
|
||||
const event = this.props.room.getAccountData("org.matrix.room.color_scheme");
|
||||
if (!event) {
|
||||
return data;
|
||||
const scheme = SettingsStore.getValueAt(SettingLevel.ROOM_ACCOUNT, "roomColor", this.props.room.roomId);
|
||||
|
||||
if (scheme.primary_color && scheme.secondary_color) {
|
||||
// We only use the user's scheme if the scheme is valid.
|
||||
data.primary_color = scheme.primary_color;
|
||||
data.secondary_color = scheme.secondary_color;
|
||||
}
|
||||
const scheme = event.getContent();
|
||||
data.primary_color = scheme.primary_color;
|
||||
data.secondary_color = scheme.secondary_color;
|
||||
data.index = this._getColorIndex(data);
|
||||
|
||||
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
|
||||
// they can't, so we still send the request but display a sensible
|
||||
// error if it fails.
|
||||
return MatrixClientPeg.get().setRoomAccountData(
|
||||
this.props.room.roomId, "org.matrix.room.color_scheme", {
|
||||
primary_color: this.state.primary_color,
|
||||
secondary_color: this.state.secondary_color,
|
||||
},
|
||||
).catch(function(err) {
|
||||
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||
// TODO: Support guests for room color. Technically this is possible via granular settings
|
||||
// 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,
|
||||
secondary_color: this.state.secondary_color,
|
||||
}).catch(function(err) {
|
||||
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -15,9 +16,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
const React = require('react');
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const UserSettingsStore = require('../../../UserSettingsStore');
|
||||
const sdk = require("../../../index");
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -27,133 +28,64 @@ module.exports = React.createClass({
|
|||
room: React.PropTypes.object,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
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() {
|
||||
const promises = [];
|
||||
|
||||
if (this.state.globalDisableUrlPreview !== this.originalState.globalDisableUrlPreview) {
|
||||
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));
|
||||
|
||||
if (this.refs.urlPreviewsRoom) promises.push(this.refs.urlPreviewsRoom.save());
|
||||
if (this.refs.urlPrviewsSelf) promises.push(this.refs.urlPreviewsSelf.save());
|
||||
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() {
|
||||
const roomState = this.props.room.currentState;
|
||||
const cli = MatrixClientPeg.get();
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
const roomId = this.props.room.roomId;
|
||||
|
||||
const maySetRoomPreviewUrls = roomState.mayClientSendStateEvent('org.matrix.room.preview_urls', cli);
|
||||
let disableRoomPreviewUrls;
|
||||
if (maySetRoomPreviewUrls) {
|
||||
disableRoomPreviewUrls =
|
||||
<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 = (
|
||||
_t("You have <a>disabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
|
||||
);
|
||||
} else {
|
||||
urlPreviewText = (
|
||||
let previewsForAccount = null;
|
||||
if (SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled")) {
|
||||
previewsForAccount = (
|
||||
_t("You have <a>enabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
|
||||
);
|
||||
} else {
|
||||
previewsForAccount = (
|
||||
_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 = "URL previews are enabled by default for participants in this room.";
|
||||
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled")) {
|
||||
str = "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 (
|
||||
<div className="mx_RoomSettings_toggles">
|
||||
<h3>{ _t("URL Previews") }</h3>
|
||||
|
||||
<label>
|
||||
{ urlPreviewText }
|
||||
</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>
|
||||
<label>{ previewsForAccount }</label>
|
||||
{ previewsForRoom }
|
||||
<label>{ previewsForRoomAccount }</label>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -24,9 +24,10 @@ import isEqual from 'lodash/isEqual';
|
|||
import sdk from '../../../index';
|
||||
import type {Completion} from '../../../autocomplete/Autocompleter';
|
||||
import Promise from 'bluebird';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
|
||||
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import Autocompleter from '../../../autocomplete/Autocompleter';
|
||||
|
||||
const COMPOSER_SELECTED = 0;
|
||||
|
@ -95,7 +96,7 @@ export default class Autocomplete extends React.Component {
|
|||
});
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
let autocompleteDelay = UserSettingsStore.getLocalSetting('autocompleteDelay', 200);
|
||||
let autocompleteDelay = SettingsStore.getValue("autocompleteDelay");
|
||||
|
||||
// Don't debounce if we are already showing completions
|
||||
if (this.state.completions.length > 0 || this.state.forceComplete) {
|
||||
|
|
|
@ -23,6 +23,7 @@ import ObjectUtils from '../../../ObjectUtils';
|
|||
import AppsDrawer from './AppsDrawer';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'AuxPanel',
|
||||
|
||||
|
|
|
@ -494,7 +494,6 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const defaultPerms = {
|
||||
can: {},
|
||||
muted: false,
|
||||
modifyLevel: false,
|
||||
};
|
||||
const room = this.props.matrixClient.getRoom(member.roomId);
|
||||
if (!room) return defaultPerms;
|
||||
|
@ -516,13 +515,15 @@ module.exports = withMatrixClient(React.createClass({
|
|||
},
|
||||
|
||||
_calculateCanPermissions: function(me, them, powerLevels) {
|
||||
const isMe = me.userId === them.userId;
|
||||
const can = {
|
||||
kick: false,
|
||||
ban: false,
|
||||
mute: false,
|
||||
modifyLevel: false,
|
||||
modifyLevelMax: 0,
|
||||
};
|
||||
const canAffectUser = them.powerLevel < me.powerLevel;
|
||||
const canAffectUser = them.powerLevel < me.powerLevel || isMe;
|
||||
if (!canAffectUser) {
|
||||
//console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
|
||||
return can;
|
||||
|
@ -531,16 +532,13 @@ module.exports = withMatrixClient(React.createClass({
|
|||
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
|
||||
powerLevels.state_default
|
||||
);
|
||||
const levelToSend = (
|
||||
(powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
|
||||
powerLevels.events_default
|
||||
);
|
||||
|
||||
can.kick = me.powerLevel >= powerLevels.kick;
|
||||
can.ban = me.powerLevel >= powerLevels.ban;
|
||||
can.mute = me.powerLevel >= editPowerLevel;
|
||||
can.toggleMod = me.powerLevel > them.powerLevel && them.powerLevel >= levelToSend;
|
||||
can.modifyLevel = me.powerLevel > them.powerLevel && me.powerLevel >= editPowerLevel;
|
||||
can.modifyLevel = me.powerLevel >= editPowerLevel && (isMe || me.powerLevel > them.powerLevel);
|
||||
can.modifyLevelMax = me.powerLevel;
|
||||
|
||||
return can;
|
||||
},
|
||||
|
||||
|
@ -832,8 +830,11 @@ module.exports = withMatrixClient(React.createClass({
|
|||
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
|
||||
const PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
|
||||
|
@ -842,7 +843,9 @@ module.exports = withMatrixClient(React.createClass({
|
|||
{ _t("Level:") } <b>
|
||||
<PowerSelector controlled={true}
|
||||
value={parseInt(this.props.member.powerLevel)}
|
||||
maxValue={this.state.can.modifyLevelMax}
|
||||
disabled={!this.state.can.modifyLevel}
|
||||
usersDefault={powerLevelUsersDefault}
|
||||
onChange={this.onPowerChange} />
|
||||
</b>
|
||||
</div>
|
||||
|
|
|
@ -22,7 +22,7 @@ import Modal from '../../../Modal';
|
|||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import Autocomplete from './Autocomplete';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
|
||||
|
||||
export default class MessageComposer extends React.Component {
|
||||
|
@ -49,10 +49,10 @@ export default class MessageComposer extends React.Component {
|
|||
inputState: {
|
||||
style: [],
|
||||
blockType: null,
|
||||
isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false),
|
||||
isRichtextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
|
||||
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() {
|
||||
UserSettingsStore.setSyncedSetting('MessageComposer.showFormatting', !this.state.showFormatting);
|
||||
SettingsStore.setValue("MessageComposer.showFormatting", null, SettingLevel.DEVICE, !this.state.showFormatting);
|
||||
this.setState({showFormatting: !this.state.showFormatting});
|
||||
}
|
||||
|
||||
|
@ -238,7 +238,7 @@ export default class MessageComposer extends React.Component {
|
|||
render() {
|
||||
const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
const uploadInputStyle = {display: 'none'};
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
const MemberPresenceAvatar = sdk.getComponent('avatars.MemberPresenceAvatar');
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput");
|
||||
|
||||
|
@ -246,7 +246,7 @@ export default class MessageComposer extends React.Component {
|
|||
|
||||
controls.push(
|
||||
<div key="controls_avatar" className="mx_MessageComposer_avatar">
|
||||
<MemberAvatar member={me} width={24} height={24} />
|
||||
<MemberPresenceAvatar member={me} width={24} height={24} />
|
||||
</div>,
|
||||
);
|
||||
|
||||
|
|
|
@ -35,7 +35,6 @@ import { _t, _td } from '../../../languageHandler';
|
|||
import Analytics from '../../../Analytics';
|
||||
|
||||
import dis from '../../../dispatcher';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
|
||||
import * as RichText from '../../../RichText';
|
||||
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');
|
||||
|
||||
import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
||||
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
||||
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
|
||||
|
@ -165,7 +165,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
|
||||
this.onTextPasted = this.onTextPasted.bind(this);
|
||||
|
||||
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
|
||||
const isRichtextEnabled = SettingsStore.getValue('MessageComposerInput.isRichTextEnabled');
|
||||
|
||||
Analytics.setRichtextMode(isRichtextEnabled);
|
||||
|
||||
|
@ -216,7 +216,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
createEditorState(richText: boolean, contentState: ?ContentState): EditorState {
|
||||
const decorators = richText ? RichText.getScopedRTDecorators(this.props) :
|
||||
RichText.getScopedMDDecorators(this.props);
|
||||
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
|
||||
const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
|
||||
decorators.push({
|
||||
strategy: this.findPillEntities.bind(this),
|
||||
component: (entityProps) => {
|
||||
|
@ -384,7 +384,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
sendTyping(isTyping) {
|
||||
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
|
||||
if (SettingsStore.getValue('dontSendTypingNotifications')) return;
|
||||
MatrixClientPeg.get().sendTyping(
|
||||
this.props.room.roomId,
|
||||
this.isTyping, TYPING_SERVER_TIMEOUT,
|
||||
|
@ -431,7 +431,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
// 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
|
||||
const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset));
|
||||
if(emojiMatch) {
|
||||
|
@ -551,7 +551,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
editorState: this.createEditorState(enabled, contentState),
|
||||
isRichtextEnabled: enabled,
|
||||
});
|
||||
UserSettingsStore.setSyncedSetting('MessageComposerInput.isRichTextEnabled', enabled);
|
||||
SettingsStore.setValue("MessageComposerInput.isRichTextEnabled", null, SettingLevel.ACCOUNT, enabled);
|
||||
}
|
||||
|
||||
handleKeyCommand = (command: string): boolean => {
|
||||
|
|
|
@ -31,7 +31,7 @@ import linkifyMatrix from '../../../linkify-matrix';
|
|||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import ManageIntegsButton from '../elements/ManageIntegsButton';
|
||||
import {CancelButton} from './SimpleRoomHeader';
|
||||
import UserSettingsStore from "../../../UserSettingsStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
|
@ -339,7 +339,7 @@ module.exports = React.createClass({
|
|||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
if (this.props.onPinnedClick && UserSettingsStore.isFeatureEnabled('feature_pinning')) {
|
||||
if (this.props.onPinnedClick && SettingsStore.isFeatureEnabled('feature_pinning')) {
|
||||
let pinsIndicator = null;
|
||||
if (this._hasUnreadPins()) {
|
||||
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />);
|
||||
|
|
|
@ -23,8 +23,8 @@ import sdk from '../../../index';
|
|||
import Modal from '../../../Modal';
|
||||
import ObjectUtils from '../../../ObjectUtils';
|
||||
import dis from '../../../dispatcher';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
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
|
||||
|
@ -309,9 +309,9 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
// url preview settings
|
||||
const ps = this.saveUrlPreviewSettings();
|
||||
let ps = this.saveUrlPreviewSettings();
|
||||
if (ps.length > 0) {
|
||||
promises.push(ps);
|
||||
ps.map(p => promises.push(p));
|
||||
}
|
||||
|
||||
// related groups
|
||||
|
@ -363,26 +363,16 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
saveBlacklistUnverifiedDevicesPerRoom: function() {
|
||||
if (!this.refs.blacklistUnverified) return;
|
||||
if (this._isRoomBlacklistUnverified() !== this.refs.blacklistUnverified.checked) {
|
||||
this._setRoomBlacklistUnverified(this.refs.blacklistUnverified.checked);
|
||||
}
|
||||
},
|
||||
|
||||
_isRoomBlacklistUnverified: function() {
|
||||
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);
|
||||
if (!this.refs.blacklistUnverifiedDevices) return;
|
||||
this.refs.blacklistUnverifiedDevices.save().then(() => {
|
||||
const value = SettingsStore.getValueAt(
|
||||
SettingLevel.ROOM_DEVICE,
|
||||
"blacklistUnverifiedDevices",
|
||||
this.props.room.roomId,
|
||||
/*explicit=*/true,
|
||||
);
|
||||
this.props.room.setBlacklistUnverifiedDevices(value);
|
||||
});
|
||||
},
|
||||
|
||||
_hasDiff: function(strA, strB) {
|
||||
|
@ -588,19 +578,20 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_renderEncryptionSection: function() {
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const roomState = this.props.room.currentState;
|
||||
const isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
|
||||
const isGlobalBlacklistUnverified = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevices;
|
||||
const isRoomBlacklistUnverified = this._isRoomBlacklistUnverified();
|
||||
|
||||
const settings =
|
||||
<label>
|
||||
<input type="checkbox" ref="blacklistUnverified"
|
||||
defaultChecked={isGlobalBlacklistUnverified || isRoomBlacklistUnverified}
|
||||
disabled={isGlobalBlacklistUnverified || (this.refs.encrypt && !this.refs.encrypt.checked)} />
|
||||
{ _t('Never send encrypted messages to unverified devices in this room from this device') }.
|
||||
</label>;
|
||||
let settings = (
|
||||
<SettingsFlag name="blacklistUnverifiedDevices"
|
||||
level={SettingLevel.ROOM_DEVICE}
|
||||
roomId={this.props.room.roomId}
|
||||
manualSave={true}
|
||||
ref="blacklistUnverifiedDevices"
|
||||
/>
|
||||
);
|
||||
|
||||
if (!isEncrypted && roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
|
||||
return (
|
||||
|
@ -908,31 +899,31 @@ module.exports = React.createClass({
|
|||
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<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 className="mx_RoomSettings_powerLevel">
|
||||
<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 className="mx_RoomSettings_powerLevel">
|
||||
<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 className="mx_RoomSettings_powerLevel">
|
||||
<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 className="mx_RoomSettings_powerLevel">
|
||||
<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 className="mx_RoomSettings_powerLevel">
|
||||
<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 className="mx_RoomSettings_powerLevel">
|
||||
<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>
|
||||
|
||||
{ Object.keys(events_levels).map(function(event_type, i) {
|
||||
|
@ -942,7 +933,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_RoomSettings_powerLevel" key={event_type}>
|
||||
<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]} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -27,7 +27,6 @@ const ContextualMenu = require('../../structures/ContextualMenu');
|
|||
const RoomNotifs = require('../../../RoomNotifs');
|
||||
const FormattingUtils = require('../../../utils/FormattingUtils');
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
const UserSettingsStore = require('../../../UserSettingsStore');
|
||||
import ActiveRoomObserver from '../../../ActiveRoomObserver';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ import classNames from 'classnames';
|
|||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'VideoView',
|
||||
|
@ -113,7 +113,7 @@ module.exports = React.createClass({
|
|||
const maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
|
||||
const localVideoFeedClasses = classNames("mx_VideoView_localVideoFeed",
|
||||
{ "mx_VideoView_localVideoFeed_flipped":
|
||||
UserSettingsStore.getSyncedSetting('VideoView.flipVideoHorizontally', false),
|
||||
SettingsStore.getValue('VideoView.flipVideoHorizontally'),
|
||||
},
|
||||
);
|
||||
return (
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
"This email address was not found": "This email address was not found",
|
||||
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Your email address does not appear to be associated with a Matrix ID on this Homeserver.",
|
||||
"Default": "Default",
|
||||
"User": "User",
|
||||
"Restricted": "Restricted",
|
||||
"Moderator": "Moderator",
|
||||
"Admin": "Admin",
|
||||
"Start a chat": "Start a chat",
|
||||
|
@ -150,8 +150,6 @@
|
|||
"%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s",
|
||||
"%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s",
|
||||
"%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s",
|
||||
"Communities": "Communities",
|
||||
"Message Pinning": "Message Pinning",
|
||||
"%(displayName)s is typing": "%(displayName)s is typing",
|
||||
"%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing",
|
||||
"%(names)s and %(count)s others are typing|one": "%(names)s and one other is typing",
|
||||
|
@ -163,6 +161,32 @@
|
|||
"Not a valid Riot keyfile": "Not a valid Riot keyfile",
|
||||
"Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?",
|
||||
"Failed to join room": "Failed to join room",
|
||||
"Message Pinning": "Message Pinning",
|
||||
"Presence Management": "Presence Management",
|
||||
"Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing",
|
||||
"Use compact timeline layout": "Use compact timeline layout",
|
||||
"Hide removed messages": "Hide removed messages",
|
||||
"Hide join/leave messages (invites/kicks/bans unaffected)": "Hide join/leave messages (invites/kicks/bans unaffected)",
|
||||
"Hide avatar changes": "Hide avatar changes",
|
||||
"Hide display name changes": "Hide display name changes",
|
||||
"Hide read receipts": "Hide read receipts",
|
||||
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
|
||||
"Always show message timestamps": "Always show message timestamps",
|
||||
"Autoplay GIFs and videos": "Autoplay GIFs and videos",
|
||||
"Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting",
|
||||
"Hide avatars in user and room mentions": "Hide avatars in user and room mentions",
|
||||
"Disable big emoji in chat": "Disable big emoji in chat",
|
||||
"Don't send typing notifications": "Don't send typing notifications",
|
||||
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
|
||||
"Mirror local video feed": "Mirror local video feed",
|
||||
"Disable Peer-to-Peer for 1:1 calls": "Disable Peer-to-Peer for 1:1 calls",
|
||||
"Opt out of analytics": "Opt out of analytics",
|
||||
"Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device",
|
||||
"Never send encrypted messages to unverified devices in this room from this device": "Never send encrypted messages to unverified devices in this room from this device",
|
||||
"Enable inline URL previews by default": "Enable inline URL previews by default",
|
||||
"Enable URL previews for this room (only affects you)": "Enable URL previews for this room (only affects you)",
|
||||
"Enable URL previews by default for participants in this room": "Enable URL previews by default for participants in this room",
|
||||
"Room Colour": "Room Colour",
|
||||
"Active call (%(roomName)s)": "Active call (%(roomName)s)",
|
||||
"unknown caller": "unknown caller",
|
||||
"Incoming voice call from %(name)s": "Incoming voice call from %(name)s",
|
||||
|
@ -370,7 +394,6 @@
|
|||
"Devices will not yet be able to decrypt history from before they joined the room": "Devices will not yet be able to decrypt history from before they joined the room",
|
||||
"Once encryption is enabled for a room it cannot be turned off again (for now)": "Once encryption is enabled for a room it cannot be turned off again (for now)",
|
||||
"Encrypted messages will not be visible on clients that do not yet implement encryption": "Encrypted messages will not be visible on clients that do not yet implement encryption",
|
||||
"Never send encrypted messages to unverified devices in this room from this device": "Never send encrypted messages to unverified devices in this room from this device",
|
||||
"Enable encryption": "Enable encryption",
|
||||
"(warning: cannot be disabled again!)": "(warning: cannot be disabled again!)",
|
||||
"Encryption is enabled in this room": "Encryption is enabled in this room",
|
||||
|
@ -396,7 +419,6 @@
|
|||
"Members only (since the point in time of selecting this option)": "Members only (since the point in time of selecting this option)",
|
||||
"Members only (since they were invited)": "Members only (since they were invited)",
|
||||
"Members only (since they joined)": "Members only (since they joined)",
|
||||
"Room Colour": "Room Colour",
|
||||
"Permissions": "Permissions",
|
||||
"The default role for new room members is": "The default role for new room members is",
|
||||
"To send messages, you must be a": "To send messages, you must be a",
|
||||
|
@ -430,15 +452,9 @@
|
|||
"Related communities for this room:": "Related communities for this room:",
|
||||
"This room has no related communities": "This room has no related communities",
|
||||
"New community ID (e.g. +foo:%(localDomain)s)": "New community ID (e.g. +foo:%(localDomain)s)",
|
||||
"Disable URL previews by default for participants in this room": "Disable URL previews by default for participants in this room",
|
||||
"URL previews are %(globalDisableUrlPreview)s by default for participants in this room.": "URL previews are %(globalDisableUrlPreview)s by default for participants in this room.",
|
||||
"disabled": "disabled",
|
||||
"enabled": "enabled",
|
||||
"You have <a>disabled</a> URL previews by default.": "You have <a>disabled</a> URL previews by default.",
|
||||
"You have <a>enabled</a> URL previews by default.": "You have <a>enabled</a> URL previews by default.",
|
||||
"You have <a>disabled</a> URL previews by default.": "You have <a>disabled</a> URL previews by default.",
|
||||
"URL Previews": "URL Previews",
|
||||
"Enable URL previews for this room (affects only you)": "Enable URL previews for this room (affects only you)",
|
||||
"Disable URL previews for this room (affects only you)": "Disable URL previews for this room (affects only you)",
|
||||
"Error decrypting audio": "Error decrypting audio",
|
||||
"Error decrypting attachment": "Error decrypting attachment",
|
||||
"Decrypt %(text)s": "Decrypt %(text)s",
|
||||
|
@ -475,6 +491,7 @@
|
|||
"Please enter the code it contains:": "Please enter the code it contains:",
|
||||
"Start authentication": "Start authentication",
|
||||
"powered by Matrix": "powered by Matrix",
|
||||
"Username on %(hs)s": "Username on %(hs)s",
|
||||
"User name": "User name",
|
||||
"Mobile phone number": "Mobile phone number",
|
||||
"Forgot your password?": "Forgot your password?",
|
||||
|
@ -524,6 +541,7 @@
|
|||
"Unverify": "Unverify",
|
||||
"Verify...": "Verify...",
|
||||
"No results": "No results",
|
||||
"Communities": "Communities",
|
||||
"Home": "Home",
|
||||
"Integrations Error": "Integrations Error",
|
||||
"Could not connect to the integration server": "Could not connect to the integration server",
|
||||
|
@ -580,6 +598,7 @@
|
|||
"%(items)s and %(count)s others|other": "%(items)s and %(count)s others",
|
||||
"%(items)s and %(count)s others|one": "%(items)s and one other",
|
||||
"%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s",
|
||||
"Custom of %(powerLevel)s": "Custom of %(powerLevel)s",
|
||||
"Custom level": "Custom level",
|
||||
"Room directory": "Room directory",
|
||||
"Start chat": "Start chat",
|
||||
|
@ -729,11 +748,11 @@
|
|||
"Scroll to bottom of page": "Scroll to bottom of page",
|
||||
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
||||
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
||||
"<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.": "<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.",
|
||||
"<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.": "<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.",
|
||||
"%(count)s new messages|other": "%(count)s new messages",
|
||||
"%(count)s new messages|one": "%(count)s new message",
|
||||
"Active call": "Active call",
|
||||
"There's no one else here! Would you like to <a>invite others</a> or <a>stop warning about the empty room</a>?": "There's no one else here! Would you like to <a>invite others</a> or <a>stop warning about the empty room</a>?",
|
||||
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
||||
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
||||
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
||||
"Some of your messages have not been sent.": "Some of your messages have not been sent.",
|
||||
|
@ -758,26 +777,9 @@
|
|||
"Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others",
|
||||
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
|
||||
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
|
||||
"Autoplay GIFs and videos": "Autoplay GIFs and videos",
|
||||
"Hide read receipts": "Hide read receipts",
|
||||
"Don't send typing notifications": "Don't send typing notifications",
|
||||
"Always show message timestamps": "Always show message timestamps",
|
||||
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
|
||||
"Hide join/leave messages (invites/kicks/bans unaffected)": "Hide join/leave messages (invites/kicks/bans unaffected)",
|
||||
"Hide avatar and display name changes": "Hide avatar and display name changes",
|
||||
"Use compact timeline layout": "Use compact timeline layout",
|
||||
"Hide removed messages": "Hide removed messages",
|
||||
"Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting",
|
||||
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
|
||||
"Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing",
|
||||
"Hide avatars in user and room mentions": "Hide avatars in user and room mentions",
|
||||
"Disable big emoji in chat": "Disable big emoji in chat",
|
||||
"Mirror local video feed": "Mirror local video feed",
|
||||
"Opt out of analytics": "Opt out of analytics",
|
||||
"Disable Peer-to-Peer for 1:1 calls": "Disable Peer-to-Peer for 1:1 calls",
|
||||
"Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device",
|
||||
"Light theme": "Light theme",
|
||||
"Dark theme": "Dark theme",
|
||||
"Status.im theme": "Status.im theme",
|
||||
"Can't load user settings": "Can't load user settings",
|
||||
"Server may be unavailable or overloaded": "Server may be unavailable or overloaded",
|
||||
"Sign out": "Sign out",
|
||||
|
@ -792,7 +794,6 @@
|
|||
"Interface Language": "Interface Language",
|
||||
"User Interface": "User Interface",
|
||||
"Autocomplete Delay (ms):": "Autocomplete Delay (ms):",
|
||||
"Disable inline URL previews by default": "Disable inline URL previews by default",
|
||||
"<not supported>": "<not supported>",
|
||||
"Import E2E room keys": "Import E2E room keys",
|
||||
"Cryptography": "Cryptography",
|
||||
|
@ -857,6 +858,7 @@
|
|||
"Create an account": "Create an account",
|
||||
"This Home Server does not support login using email address.": "This Home Server does not support login using email address.",
|
||||
"Incorrect username and/or password.": "Incorrect username and/or password.",
|
||||
"Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.",
|
||||
"Guest access is disabled on this Home Server.": "Guest access is disabled on this Home Server.",
|
||||
"The phone number entered looks invalid": "The phone number entered looks invalid",
|
||||
"Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.",
|
||||
|
@ -864,7 +866,7 @@
|
|||
"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.": "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.",
|
||||
"Sorry, this homeserver is using a login which is not recognised ": "Sorry, this homeserver is using a login which is not recognised ",
|
||||
"Login as guest": "Login as guest",
|
||||
"Return to app": "Return to app",
|
||||
"Sign in to get started": "Sign in to get started",
|
||||
"Failed to fetch avatar URL": "Failed to fetch avatar URL",
|
||||
"Set a display name:": "Set a display name:",
|
||||
"Upload an avatar:": "Upload an avatar:",
|
||||
|
|
|
@ -19,8 +19,7 @@ import request from 'browser-request';
|
|||
import counterpart from 'counterpart';
|
||||
import Promise from 'bluebird';
|
||||
import React from 'react';
|
||||
|
||||
import UserSettingsStore from './UserSettingsStore';
|
||||
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
||||
|
||||
const i18nFolder = 'i18n/';
|
||||
|
||||
|
@ -226,7 +225,7 @@ export function setLanguage(preferredLangs) {
|
|||
}).then((langData) => {
|
||||
counterpart.registerTranslations(langToUse, langData);
|
||||
counterpart.setLocale(langToUse);
|
||||
UserSettingsStore.setLocalSetting('language', langToUse);
|
||||
SettingsStore.setValue("language", null, SettingLevel.DEVICE, langToUse);
|
||||
console.log("set language to " + langToUse);
|
||||
|
||||
// Set 'en' as fallback language:
|
||||
|
|
252
src/settings/Settings.js
Normal file
252
src/settings/Settings.js
Normal file
|
@ -0,0 +1,252 @@
|
|||
/*
|
||||
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 {_td} from '../languageHandler';
|
||||
import {
|
||||
AudioNotificationsEnabledController,
|
||||
NotificationBodyEnabledController,
|
||||
NotificationsEnabledController,
|
||||
} from "./controllers/NotificationControllers";
|
||||
|
||||
|
||||
// These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
|
||||
const LEVELS_ROOM_SETTINGS = ['device', 'room-device', 'room-account', 'account', 'config'];
|
||||
const LEVELS_ROOM_SETTINGS_WITH_ROOM = ['device', 'room-device', 'room-account', 'account', 'config', 'room'];
|
||||
const LEVELS_ACCOUNT_SETTINGS = ['device', 'account', 'config'];
|
||||
const LEVELS_FEATURE = ['device', 'config'];
|
||||
const LEVELS_DEVICE_ONLY_SETTINGS = ['device'];
|
||||
const LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG = ['device', 'config'];
|
||||
|
||||
export const SETTINGS = {
|
||||
// EXAMPLE SETTING:
|
||||
// "my-setting": {
|
||||
// // Must be set to true for features. Default is 'false'.
|
||||
// isFeature: false,
|
||||
//
|
||||
// // Display names are strongly recommended for clarity.
|
||||
// displayName: _td("Cool Name"),
|
||||
//
|
||||
// // Display name can also be an object for different levels.
|
||||
// //displayName: {
|
||||
// // "device": _td("Name for when the setting is used at 'device'"),
|
||||
// // "room": _td("Name for when the setting is used at 'room'"),
|
||||
// // "default": _td("The name for all other levels"),
|
||||
// //}
|
||||
//
|
||||
// // The supported levels are required. Preferably, use the preset arrays
|
||||
// // at the top of this file to define this rather than a custom array.
|
||||
// supportedLevels: [
|
||||
// // The order does not matter.
|
||||
//
|
||||
// "device", // Affects the current device only
|
||||
// "room-device", // Affects the current room on the current device
|
||||
// "room-account", // Affects the current room for the current account
|
||||
// "account", // Affects the current account
|
||||
// "room", // Affects the current room (controlled by room admins)
|
||||
// "config", // Affects the current application
|
||||
//
|
||||
// // "default" is always supported and does not get listed here.
|
||||
// ],
|
||||
//
|
||||
// // Required. Can be any data type. The value specified here should match
|
||||
// // the data being stored (ie: if a boolean is used, the setting should
|
||||
// // represent a boolean).
|
||||
// default: {
|
||||
// your: "value",
|
||||
// },
|
||||
//
|
||||
// // Optional settings controller. See SettingsController for more information.
|
||||
// controller: new MySettingController(),
|
||||
//
|
||||
// // Optional flag to make supportedLevels be respected as the order to handle
|
||||
// // settings. The first element is treated as "most preferred". The "default"
|
||||
// // level is always appended to the end.
|
||||
// supportedLevelsAreOrdered: false,
|
||||
// },
|
||||
"feature_pinning": {
|
||||
isFeature: true,
|
||||
displayName: _td("Message Pinning"),
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
default: false,
|
||||
},
|
||||
"feature_presence_management": {
|
||||
isFeature: true,
|
||||
displayName: _td("Presence Management"),
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
default: false,
|
||||
},
|
||||
"MessageComposerInput.dontSuggestEmoji": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Disable Emoji suggestions while typing'),
|
||||
default: false,
|
||||
},
|
||||
"useCompactLayout": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Use compact timeline layout'),
|
||||
default: false,
|
||||
},
|
||||
"hideRedactions": {
|
||||
supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
|
||||
displayName: _td('Hide removed messages'),
|
||||
default: false,
|
||||
},
|
||||
"hideJoinLeaves": {
|
||||
supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
|
||||
displayName: _td('Hide join/leave messages (invites/kicks/bans unaffected)'),
|
||||
default: false,
|
||||
},
|
||||
"hideAvatarChanges": {
|
||||
supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
|
||||
displayName: _td('Hide avatar changes'),
|
||||
default: false,
|
||||
},
|
||||
"hideDisplaynameChanges": {
|
||||
supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
|
||||
displayName: _td('Hide display name changes'),
|
||||
default: false,
|
||||
},
|
||||
"hideReadReceipts": {
|
||||
supportedLevels: LEVELS_ROOM_SETTINGS,
|
||||
displayName: _td('Hide read receipts'),
|
||||
default: false,
|
||||
},
|
||||
"showTwelveHourTimestamps": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Show timestamps in 12 hour format (e.g. 2:30pm)'),
|
||||
default: false,
|
||||
},
|
||||
"alwaysShowTimestamps": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Always show message timestamps'),
|
||||
default: false,
|
||||
},
|
||||
"autoplayGifsAndVideos": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Autoplay GIFs and videos'),
|
||||
default: false,
|
||||
},
|
||||
"enableSyntaxHighlightLanguageDetection": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Enable automatic language detection for syntax highlighting'),
|
||||
default: false,
|
||||
},
|
||||
"Pill.shouldHidePillAvatar": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Hide avatars in user and room mentions'),
|
||||
default: false,
|
||||
},
|
||||
"TextualBody.disableBigEmoji": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Disable big emoji in chat'),
|
||||
default: false,
|
||||
},
|
||||
"MessageComposerInput.isRichTextEnabled": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
default: false,
|
||||
},
|
||||
"MessageComposer.showFormatting": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
default: false,
|
||||
},
|
||||
"dontSendTypingNotifications": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td("Don't send typing notifications"),
|
||||
default: false,
|
||||
},
|
||||
"MessageComposerInput.autoReplaceEmoji": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Automatically replace plain text Emoji'),
|
||||
default: false,
|
||||
},
|
||||
"VideoView.flipVideoHorizontally": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Mirror local video feed'),
|
||||
default: false,
|
||||
},
|
||||
"theme": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
default: "light",
|
||||
},
|
||||
"webRtcForceTURN": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||
displayName: _td('Disable Peer-to-Peer for 1:1 calls'),
|
||||
default: false,
|
||||
},
|
||||
"webrtc_audioinput": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
default: null,
|
||||
},
|
||||
"webrtc_videoinput": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
default: null,
|
||||
},
|
||||
"language": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||
default: "en",
|
||||
},
|
||||
"analyticsOptOut": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||
displayName: _td('Opt out of analytics'),
|
||||
default: false,
|
||||
},
|
||||
"autocompleteDelay": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||
default: 200,
|
||||
},
|
||||
"blacklistUnverifiedDevices": {
|
||||
// We specifically want to have room-device > device so that users may set a device default
|
||||
// with a per-room override.
|
||||
supportedLevels: ['room-device', 'device'],
|
||||
supportedLevelsAreOrdered: true,
|
||||
displayName: {
|
||||
"default": _td('Never send encrypted messages to unverified devices from this device'),
|
||||
"room-device": _td('Never send encrypted messages to unverified devices in this room from this device'),
|
||||
},
|
||||
default: false,
|
||||
},
|
||||
"urlPreviewsEnabled": {
|
||||
supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
|
||||
displayName: {
|
||||
"default": _td('Enable inline URL previews by default'),
|
||||
"room-account": _td("Enable URL previews for this room (only affects you)"),
|
||||
"room": _td("Enable URL previews by default for participants in this room"),
|
||||
},
|
||||
default: true,
|
||||
},
|
||||
"roomColor": {
|
||||
supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
|
||||
displayName: _td("Room Colour"),
|
||||
default: {
|
||||
primary_color: null, // Hex string, eg: #000000
|
||||
secondary_color: null, // Hex string, eg: #000000
|
||||
},
|
||||
},
|
||||
"notificationsEnabled": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
default: false,
|
||||
controller: new NotificationsEnabledController(),
|
||||
},
|
||||
"notificationBodyEnabled": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
default: true,
|
||||
controller: new NotificationBodyEnabledController(),
|
||||
},
|
||||
"audioNotificationsEnabled": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
default: true,
|
||||
controller: new AudioNotificationsEnabledController(),
|
||||
},
|
||||
};
|
347
src/settings/SettingsStore.js
Normal file
347
src/settings/SettingsStore.js
Normal file
|
@ -0,0 +1,347 @@
|
|||
/*
|
||||
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 DeviceSettingsHandler from "./handlers/DeviceSettingsHandler";
|
||||
import RoomDeviceSettingsHandler from "./handlers/RoomDeviceSettingsHandler";
|
||||
import DefaultSettingsHandler from "./handlers/DefaultSettingsHandler";
|
||||
import RoomAccountSettingsHandler from "./handlers/RoomAccountSettingsHandler";
|
||||
import AccountSettingsHandler from "./handlers/AccountSettingsHandler";
|
||||
import RoomSettingsHandler from "./handlers/RoomSettingsHandler";
|
||||
import ConfigSettingsHandler from "./handlers/ConfigSettingsHandler";
|
||||
import {_t} from '../languageHandler';
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import {SETTINGS} from "./Settings";
|
||||
import LocalEchoWrapper from "./handlers/LocalEchoWrapper";
|
||||
|
||||
/**
|
||||
* Represents the various setting levels supported by the SettingsStore.
|
||||
*/
|
||||
export const SettingLevel = {
|
||||
// Note: This enum is not used in this class or in the Settings file
|
||||
// This should always be used elsewhere in the project.
|
||||
DEVICE: "device",
|
||||
ROOM_DEVICE: "room-device",
|
||||
ROOM_ACCOUNT: "room-account",
|
||||
ACCOUNT: "account",
|
||||
ROOM: "room",
|
||||
CONFIG: "config",
|
||||
DEFAULT: "default",
|
||||
};
|
||||
|
||||
// Convert the settings to easier to manage objects for the handlers
|
||||
const defaultSettings = {};
|
||||
const featureNames = [];
|
||||
for (const key of Object.keys(SETTINGS)) {
|
||||
defaultSettings[key] = SETTINGS[key].default;
|
||||
if (SETTINGS[key].isFeature) featureNames.push(key);
|
||||
}
|
||||
|
||||
const LEVEL_HANDLERS = {
|
||||
"device": new DeviceSettingsHandler(featureNames),
|
||||
"room-device": new RoomDeviceSettingsHandler(),
|
||||
"room-account": new RoomAccountSettingsHandler(),
|
||||
"account": new AccountSettingsHandler(),
|
||||
"room": new RoomSettingsHandler(),
|
||||
"config": new ConfigSettingsHandler(),
|
||||
"default": new DefaultSettingsHandler(defaultSettings),
|
||||
};
|
||||
|
||||
// Wrap all the handlers with local echo
|
||||
for (const key of Object.keys(LEVEL_HANDLERS)) {
|
||||
LEVEL_HANDLERS[key] = new LocalEchoWrapper(LEVEL_HANDLERS[key]);
|
||||
}
|
||||
|
||||
const LEVEL_ORDER = [
|
||||
'device', 'room-device', 'room-account', 'account', 'room', 'config', 'default',
|
||||
];
|
||||
|
||||
/**
|
||||
* Controls and manages application settings by providing varying levels at which the
|
||||
* setting value may be specified. The levels are then used to determine what the setting
|
||||
* value should be given a set of circumstances. The levels, in priority order, are:
|
||||
* - "device" - Values are determined by the current device
|
||||
* - "room-device" - Values are determined by the current device for a particular room
|
||||
* - "room-account" - Values are determined by the current account for a particular room
|
||||
* - "account" - Values are determined by the current account
|
||||
* - "room" - Values are determined by a particular room (by the room admins)
|
||||
* - "config" - Values are determined by the config.json
|
||||
* - "default" - Values are determined by the hardcoded defaults
|
||||
*
|
||||
* Each level has a different method to storing the setting value. For implementation
|
||||
* specific details, please see the handlers. The "config" and "default" levels are
|
||||
* both always supported on all platforms. All other settings should be guarded by
|
||||
* isLevelSupported() prior to attempting to set the value.
|
||||
*
|
||||
* Settings can also represent features. Features are significant portions of the
|
||||
* application that warrant a dedicated setting to toggle them on or off. Features are
|
||||
* special-cased to ensure that their values respect the configuration (for example, a
|
||||
* feature may be reported as disabled even though a user has specifically requested it
|
||||
* be enabled).
|
||||
*/
|
||||
export default class SettingsStore {
|
||||
/**
|
||||
* Gets the translated display name for a given setting
|
||||
* @param {string} settingName The setting to look up.
|
||||
* @param {"device"|"room-device"|"room-account"|"account"|"room"|"config"|"default"} atLevel
|
||||
* The level to get the display name for; Defaults to 'default'.
|
||||
* @return {String} The display name for the setting, or null if not found.
|
||||
*/
|
||||
static getDisplayName(settingName, atLevel = "default") {
|
||||
if (!SETTINGS[settingName] || !SETTINGS[settingName].displayName) return null;
|
||||
|
||||
let displayName = SETTINGS[settingName].displayName;
|
||||
if (displayName instanceof Object) {
|
||||
if (displayName[atLevel]) displayName = displayName[atLevel];
|
||||
else displayName = displayName["default"];
|
||||
}
|
||||
|
||||
return _t(displayName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all available labs feature names
|
||||
* @returns {string[]} The list of available feature names
|
||||
*/
|
||||
static getLabsFeatures() {
|
||||
const possibleFeatures = Object.keys(SETTINGS).filter((s) => SettingsStore.isFeature(s));
|
||||
|
||||
const enableLabs = SdkConfig.get()["enableLabs"];
|
||||
if (enableLabs) return possibleFeatures;
|
||||
|
||||
return possibleFeatures.filter((s) => SettingsStore._getFeatureState(s) === "labs");
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a setting is also a feature.
|
||||
* @param {string} settingName The setting to look up.
|
||||
* @return {boolean} True if the setting is a feature.
|
||||
*/
|
||||
static isFeature(settingName) {
|
||||
if (!SETTINGS[settingName]) return false;
|
||||
return SETTINGS[settingName].isFeature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given feature is enabled. The feature given must be a known
|
||||
* feature.
|
||||
* @param {string} settingName The name of the setting that is a feature.
|
||||
* @param {String} roomId The optional room ID to validate in, may be null.
|
||||
* @return {boolean} True if the feature is enabled, false otherwise
|
||||
*/
|
||||
static isFeatureEnabled(settingName, roomId = null) {
|
||||
if (!SettingsStore.isFeature(settingName)) {
|
||||
throw new Error("Setting " + settingName + " is not a feature");
|
||||
}
|
||||
|
||||
return SettingsStore.getValue(settingName, roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a feature as enabled or disabled on the current device.
|
||||
* @param {string} settingName The name of the setting.
|
||||
* @param {boolean} value True to enable the feature, false otherwise.
|
||||
* @returns {Promise} Resolves when the setting has been set.
|
||||
*/
|
||||
static setFeatureEnabled(settingName, value) {
|
||||
// Verify that the setting is actually a setting
|
||||
if (!SETTINGS[settingName]) {
|
||||
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
|
||||
}
|
||||
if (!SettingsStore.isFeature(settingName)) {
|
||||
throw new Error("Setting " + settingName + " is not a feature");
|
||||
}
|
||||
|
||||
return SettingsStore.setValue(settingName, null, "device", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of a setting. The room ID is optional if the setting is not to
|
||||
* be applied to any particular room, otherwise it should be supplied.
|
||||
* @param {string} settingName The name of the setting to read the value of.
|
||||
* @param {String} roomId The room ID to read the setting value in, may be null.
|
||||
* @param {boolean} excludeDefault True to disable using the default value.
|
||||
* @return {*} The value, or null if not found
|
||||
*/
|
||||
static getValue(settingName, roomId = null, excludeDefault = false) {
|
||||
return SettingsStore.getValueAt(LEVEL_ORDER[0], settingName, roomId, false, excludeDefault);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a setting's value at a particular level, ignoring all levels that are more specific.
|
||||
* @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level to
|
||||
* look at.
|
||||
* @param {string} settingName The name of the setting to read.
|
||||
* @param {String} roomId The room ID to read the setting value in, may be null.
|
||||
* @param {boolean} explicit If true, this method will not consider other levels, just the one
|
||||
* provided. Defaults to false.
|
||||
* @param {boolean} excludeDefault True to disable using the default value.
|
||||
* @return {*} The value, or null if not found.
|
||||
*/
|
||||
static getValueAt(level, settingName, roomId = null, explicit = false, excludeDefault = false) {
|
||||
// Verify that the setting is actually a setting
|
||||
if (!SETTINGS[settingName]) {
|
||||
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
|
||||
}
|
||||
|
||||
const setting = SETTINGS[settingName];
|
||||
const levelOrder = (setting.supportedLevelsAreOrdered ? setting.supportedLevels : LEVEL_ORDER);
|
||||
if (!levelOrder.includes("default")) levelOrder.push("default"); // always include default
|
||||
|
||||
const minIndex = levelOrder.indexOf(level);
|
||||
if (minIndex === -1) throw new Error("Level " + level + " is not prioritized");
|
||||
|
||||
if (SettingsStore.isFeature(settingName)) {
|
||||
const configValue = SettingsStore._getFeatureState(settingName);
|
||||
if (configValue === "enable") return true;
|
||||
if (configValue === "disable") return false;
|
||||
// else let it fall through the default process
|
||||
}
|
||||
|
||||
const handlers = SettingsStore._getHandlers(settingName);
|
||||
|
||||
if (explicit) {
|
||||
const handler = handlers[level];
|
||||
if (!handler) return SettingsStore._tryControllerOverride(settingName, level, roomId, null);
|
||||
const value = handler.getValue(settingName, roomId);
|
||||
return SettingsStore._tryControllerOverride(settingName, level, roomId, value);
|
||||
}
|
||||
|
||||
for (let i = minIndex; i < levelOrder.length; i++) {
|
||||
const handler = handlers[levelOrder[i]];
|
||||
if (!handler) continue;
|
||||
if (excludeDefault && levelOrder[i] === "default") continue;
|
||||
|
||||
const value = handler.getValue(settingName, roomId);
|
||||
if (value === null || value === undefined) continue;
|
||||
return SettingsStore._tryControllerOverride(settingName, level, roomId, value);
|
||||
}
|
||||
|
||||
return SettingsStore._tryControllerOverride(settingName, level, roomId, null);
|
||||
}
|
||||
|
||||
static _tryControllerOverride(settingName, level, roomId, calculatedValue) {
|
||||
const controller = SETTINGS[settingName].controller;
|
||||
if (!controller) return calculatedValue;
|
||||
|
||||
const actualValue = controller.getValueOverride(level, roomId, calculatedValue);
|
||||
if (actualValue !== undefined && actualValue !== null) return actualValue;
|
||||
return calculatedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value for a setting. The room ID is optional if the setting is not being
|
||||
* set for a particular room, otherwise it should be supplied. The value may be null
|
||||
* to indicate that the level should no longer have an override.
|
||||
* @param {string} settingName The name of the setting to change.
|
||||
* @param {String} roomId The room ID to change the value in, may be null.
|
||||
* @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level
|
||||
* to change the value at.
|
||||
* @param {*} value The new value of the setting, may be null.
|
||||
* @return {Promise} Resolves when the setting has been changed.
|
||||
*/
|
||||
static setValue(settingName, roomId, level, value) {
|
||||
// Verify that the setting is actually a setting
|
||||
if (!SETTINGS[settingName]) {
|
||||
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
|
||||
}
|
||||
|
||||
const handler = SettingsStore._getHandler(settingName, level);
|
||||
if (!handler) {
|
||||
throw new Error("Setting " + settingName + " does not have a handler for " + level);
|
||||
}
|
||||
|
||||
if (!handler.canSetValue(settingName, roomId)) {
|
||||
throw new Error("User cannot set " + settingName + " at " + level + " in " + roomId);
|
||||
}
|
||||
|
||||
return handler.setValue(settingName, roomId, value).then(() => {
|
||||
const controller = SETTINGS[settingName].controller;
|
||||
if (!controller) return;
|
||||
controller.onChange(level, roomId, value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current user is permitted to set the given setting at the given
|
||||
* level for a particular room. The room ID is optional if the setting is not being
|
||||
* set for a particular room, otherwise it should be supplied.
|
||||
* @param {string} settingName The name of the setting to check.
|
||||
* @param {String} roomId The room ID to check in, may be null.
|
||||
* @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level to
|
||||
* check at.
|
||||
* @return {boolean} True if the user may set the setting, false otherwise.
|
||||
*/
|
||||
static canSetValue(settingName, roomId, level) {
|
||||
// Verify that the setting is actually a setting
|
||||
if (!SETTINGS[settingName]) {
|
||||
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
|
||||
}
|
||||
|
||||
const handler = SettingsStore._getHandler(settingName, level);
|
||||
if (!handler) return false;
|
||||
return handler.canSetValue(settingName, roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the given level is supported on this device.
|
||||
* @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level
|
||||
* to check the feasibility of.
|
||||
* @return {boolean} True if the level is supported, false otherwise.
|
||||
*/
|
||||
static isLevelSupported(level) {
|
||||
if (!LEVEL_HANDLERS[level]) return false;
|
||||
return LEVEL_HANDLERS[level].isSupported();
|
||||
}
|
||||
|
||||
static _getHandler(settingName, level) {
|
||||
const handlers = SettingsStore._getHandlers(settingName);
|
||||
if (!handlers[level]) return null;
|
||||
return handlers[level];
|
||||
}
|
||||
|
||||
static _getHandlers(settingName) {
|
||||
if (!SETTINGS[settingName]) return {};
|
||||
|
||||
const handlers = {};
|
||||
for (const level of SETTINGS[settingName].supportedLevels) {
|
||||
if (!LEVEL_HANDLERS[level]) throw new Error("Unexpected level " + level);
|
||||
handlers[level] = LEVEL_HANDLERS[level];
|
||||
}
|
||||
|
||||
// Always support 'default'
|
||||
if (!handlers['default']) handlers['default'] = LEVEL_HANDLERS['default'];
|
||||
|
||||
return handlers;
|
||||
}
|
||||
|
||||
static _getFeatureState(settingName) {
|
||||
const featuresConfig = SdkConfig.get()['features'];
|
||||
const enableLabs = SdkConfig.get()['enableLabs']; // we'll honour the old flag
|
||||
|
||||
let featureState = enableLabs ? "labs" : "disable";
|
||||
if (featuresConfig && featuresConfig[settingName] !== undefined) {
|
||||
featureState = featuresConfig[settingName];
|
||||
}
|
||||
|
||||
const allowedStates = ['enable', 'disable', 'labs'];
|
||||
if (!allowedStates.includes(featureState)) {
|
||||
console.warn("Feature state '" + featureState + "' is invalid for " + settingName);
|
||||
featureState = "disable"; // to prevent accidental features.
|
||||
}
|
||||
|
||||
return featureState;
|
||||
}
|
||||
}
|
49
src/settings/controllers/NotificationControllers.js
Normal file
49
src/settings/controllers/NotificationControllers.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
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 SettingController from "./SettingController";
|
||||
|
||||
export class NotificationsEnabledController extends SettingController {
|
||||
getValueOverride(level, roomId, calculatedValue) {
|
||||
const Notifier = require('../../Notifier'); // avoids cyclical references
|
||||
|
||||
return calculatedValue && Notifier.isPossible();
|
||||
}
|
||||
|
||||
onChange(level, roomId, newValue) {
|
||||
const Notifier = require('../../Notifier'); // avoids cyclical references
|
||||
|
||||
if (Notifier.supportsDesktopNotifications()) {
|
||||
Notifier.setEnabled(newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NotificationBodyEnabledController extends SettingController {
|
||||
getValueOverride(level, roomId, calculatedValue) {
|
||||
const Notifier = require('../../Notifier'); // avoids cyclical references
|
||||
|
||||
return calculatedValue && Notifier.isEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
export class AudioNotificationsEnabledController extends SettingController {
|
||||
getValueOverride(level, roomId, calculatedValue) {
|
||||
const Notifier = require('../../Notifier'); // avoids cyclical references
|
||||
|
||||
return calculatedValue && Notifier.isEnabled();
|
||||
}
|
||||
}
|
49
src/settings/controllers/SettingController.js
Normal file
49
src/settings/controllers/SettingController.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a controller for individual settings to alter the reading behaviour
|
||||
* based upon environmental conditions, or to react to changes and therefore update
|
||||
* the working environment.
|
||||
*
|
||||
* This is not intended to replace the functionality of a SettingsHandler, it is only
|
||||
* intended to handle environmental factors for specific settings.
|
||||
*/
|
||||
export default class SettingController {
|
||||
|
||||
/**
|
||||
* Gets the overridden value for the setting, if any. This must return null if the
|
||||
* value is not to be overridden, otherwise it must return the new value.
|
||||
* @param {string} level The level at which the value was requested at.
|
||||
* @param {String} roomId The room ID, may be null.
|
||||
* @param {*} calculatedValue The value that the handlers think the setting should be,
|
||||
* may be null.
|
||||
* @return {*} The value that should be used, or null if no override is applicable.
|
||||
*/
|
||||
getValueOverride(level, roomId, calculatedValue) {
|
||||
return null; // no override
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the setting value has been changed.
|
||||
* @param {string} level The level at which the setting has been modified.
|
||||
* @param {String} roomId The room ID, may be null.
|
||||
* @param {*} newValue The new value for the setting, may be null.
|
||||
*/
|
||||
onChange(level, roomId, newValue) {
|
||||
// do nothing by default
|
||||
}
|
||||
}
|
73
src/settings/handlers/AccountSettingsHandler.js
Normal file
73
src/settings/handlers/AccountSettingsHandler.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
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 SettingsHandler from "./SettingsHandler";
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
|
||||
/**
|
||||
* Gets and sets settings at the "account" level for the current user.
|
||||
* This handler does not make use of the roomId parameter.
|
||||
*/
|
||||
export default class AccountSettingHandler extends SettingsHandler {
|
||||
getValue(settingName, roomId) {
|
||||
// Special case URL previews
|
||||
if (settingName === "urlPreviewsEnabled") {
|
||||
const content = this._getSettings("org.matrix.preview_urls");
|
||||
return !content['disable'];
|
||||
}
|
||||
|
||||
let preferredValue = this._getSettings()[settingName];
|
||||
|
||||
if (preferredValue === null || preferredValue === undefined) {
|
||||
// Honour the old setting on read only
|
||||
if (settingName === "hideAvatarChanges" || settingName === "hideDisplaynameChanges") {
|
||||
preferredValue = this._getSettings()["hideAvatarDisplaynameChanges"];
|
||||
}
|
||||
}
|
||||
|
||||
return preferredValue;
|
||||
}
|
||||
|
||||
setValue(settingName, roomId, newValue) {
|
||||
// Special case URL previews
|
||||
if (settingName === "urlPreviewsEnabled") {
|
||||
const content = this._getSettings("org.matrix.preview_urls");
|
||||
content['disable'] = !newValue;
|
||||
return MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", content);
|
||||
}
|
||||
|
||||
const content = this._getSettings();
|
||||
content[settingName] = newValue;
|
||||
return MatrixClientPeg.get().setAccountData("im.vector.web.settings", content);
|
||||
}
|
||||
|
||||
canSetValue(settingName, roomId) {
|
||||
return true; // It's their account, so they should be able to
|
||||
}
|
||||
|
||||
isSupported() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli !== undefined && cli !== null;
|
||||
}
|
||||
|
||||
_getSettings(eventType = "im.vector.web.settings") {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli) return {};
|
||||
const event = cli.getAccountData(eventType);
|
||||
if (!event || !event.getContent()) return {};
|
||||
return event.getContent();
|
||||
}
|
||||
}
|
49
src/settings/handlers/ConfigSettingsHandler.js
Normal file
49
src/settings/handlers/ConfigSettingsHandler.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
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 SettingsHandler from "./SettingsHandler";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
|
||||
/**
|
||||
* Gets and sets settings at the "config" level. This handler does not make use of the
|
||||
* roomId parameter.
|
||||
*/
|
||||
export default class ConfigSettingsHandler extends SettingsHandler {
|
||||
getValue(settingName, roomId) {
|
||||
const config = SdkConfig.get() || {};
|
||||
|
||||
// Special case themes
|
||||
if (settingName === "theme") {
|
||||
return config["default_theme"];
|
||||
}
|
||||
|
||||
const settingsConfig = config["settingDefaults"];
|
||||
if (!settingsConfig || !settingsConfig[settingName]) return null;
|
||||
return settingsConfig[settingName];
|
||||
}
|
||||
|
||||
setValue(settingName, roomId, newValue) {
|
||||
throw new Error("Cannot change settings at the config level");
|
||||
}
|
||||
|
||||
canSetValue(settingName, roomId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
isSupported() {
|
||||
return true; // SdkConfig is always there
|
||||
}
|
||||
}
|
48
src/settings/handlers/DefaultSettingsHandler.js
Normal file
48
src/settings/handlers/DefaultSettingsHandler.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
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 SettingsHandler from "./SettingsHandler";
|
||||
|
||||
/**
|
||||
* Gets settings at the "default" level. This handler does not support setting values.
|
||||
* This handler does not make use of the roomId parameter.
|
||||
*/
|
||||
export default class DefaultSettingsHandler extends SettingsHandler {
|
||||
/**
|
||||
* Creates a new default settings handler with the given defaults
|
||||
* @param {object} defaults The default setting values, keyed by setting name.
|
||||
*/
|
||||
constructor(defaults) {
|
||||
super();
|
||||
this._defaults = defaults;
|
||||
}
|
||||
|
||||
getValue(settingName, roomId) {
|
||||
return this._defaults[settingName];
|
||||
}
|
||||
|
||||
setValue(settingName, roomId, newValue) {
|
||||
throw new Error("Cannot set values on the default level handler");
|
||||
}
|
||||
|
||||
canSetValue(settingName, roomId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
isSupported() {
|
||||
return true;
|
||||
}
|
||||
}
|
108
src/settings/handlers/DeviceSettingsHandler.js
Normal file
108
src/settings/handlers/DeviceSettingsHandler.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
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 Promise from 'bluebird';
|
||||
import SettingsHandler from "./SettingsHandler";
|
||||
import MatrixClientPeg from "../../MatrixClientPeg";
|
||||
|
||||
/**
|
||||
* Gets and sets settings at the "device" level for the current device.
|
||||
* This handler does not make use of the roomId parameter. This handler
|
||||
* will special-case features to support legacy settings.
|
||||
*/
|
||||
export default class DeviceSettingsHandler extends SettingsHandler {
|
||||
/**
|
||||
* Creates a new device settings handler
|
||||
* @param {string[]} featureNames The names of known features.
|
||||
*/
|
||||
constructor(featureNames) {
|
||||
super();
|
||||
this._featureNames = featureNames;
|
||||
}
|
||||
|
||||
getValue(settingName, roomId) {
|
||||
if (this._featureNames.includes(settingName)) {
|
||||
return this._readFeature(settingName);
|
||||
}
|
||||
|
||||
// Special case notifications
|
||||
if (settingName === "notificationsEnabled") {
|
||||
return localStorage.getItem("notifications_enabled") === "true";
|
||||
} else if (settingName === "notificationBodyEnabled") {
|
||||
return localStorage.getItem("notifications_body_enabled") === "true";
|
||||
} else if (settingName === "audioNotificationsEnabled") {
|
||||
return localStorage.getItem("audio_notifications_enabled") === "true";
|
||||
}
|
||||
|
||||
return this._getSettings()[settingName];
|
||||
}
|
||||
|
||||
setValue(settingName, roomId, newValue) {
|
||||
if (this._featureNames.includes(settingName)) {
|
||||
this._writeFeature(settingName, newValue);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Special case notifications
|
||||
if (settingName === "notificationsEnabled") {
|
||||
localStorage.setItem("notifications_enabled", newValue);
|
||||
return Promise.resolve();
|
||||
} else if (settingName === "notificationBodyEnabled") {
|
||||
localStorage.setItem("notifications_body_enabled", newValue);
|
||||
return Promise.resolve();
|
||||
} else if (settingName === "audioNotificationsEnabled") {
|
||||
localStorage.setItem("audio_notifications_enabled", newValue);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const settings = this._getSettings();
|
||||
settings[settingName] = newValue;
|
||||
localStorage.setItem("mx_local_settings", JSON.stringify(settings));
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
canSetValue(settingName, roomId) {
|
||||
return true; // It's their device, so they should be able to
|
||||
}
|
||||
|
||||
isSupported() {
|
||||
return localStorage !== undefined && localStorage !== null;
|
||||
}
|
||||
|
||||
_getSettings() {
|
||||
const value = localStorage.getItem("mx_local_settings");
|
||||
if (!value) return {};
|
||||
return JSON.parse(value);
|
||||
}
|
||||
|
||||
// Note: features intentionally don't use the same key as settings to avoid conflicts
|
||||
// and to be backwards compatible.
|
||||
|
||||
_readFeature(featureName) {
|
||||
if (MatrixClientPeg.get() && MatrixClientPeg.get().isGuest()) {
|
||||
// Guests should not have any labs features enabled.
|
||||
return {enabled: false};
|
||||
}
|
||||
|
||||
const value = localStorage.getItem("mx_labs_feature_" + featureName);
|
||||
return value === "true";
|
||||
}
|
||||
|
||||
_writeFeature(featureName, enabled) {
|
||||
localStorage.setItem("mx_labs_feature_" + featureName, enabled);
|
||||
}
|
||||
}
|
69
src/settings/handlers/LocalEchoWrapper.js
Normal file
69
src/settings/handlers/LocalEchoWrapper.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
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 Promise from "bluebird";
|
||||
import SettingsHandler from "./SettingsHandler";
|
||||
|
||||
/**
|
||||
* A wrapper for a SettingsHandler that performs local echo on
|
||||
* changes to settings. This wrapper will use the underlying
|
||||
* handler as much as possible to ensure values are not stale.
|
||||
*/
|
||||
export default class LocalEchoWrapper extends SettingsHandler {
|
||||
/**
|
||||
* Creates a new local echo wrapper
|
||||
* @param {SettingsHandler} handler The handler to wrap
|
||||
*/
|
||||
constructor(handler) {
|
||||
super();
|
||||
this._handler = handler;
|
||||
this._cache = {
|
||||
// settingName: { roomId: value }
|
||||
};
|
||||
}
|
||||
|
||||
getValue(settingName, roomId) {
|
||||
const cacheRoomId = roomId ? roomId : "UNDEFINED"; // avoid weird keys
|
||||
const bySetting = this._cache[settingName];
|
||||
if (bySetting && bySetting.hasOwnProperty(cacheRoomId)) {
|
||||
return bySetting[roomId];
|
||||
}
|
||||
|
||||
return this._handler.getValue(settingName, roomId);
|
||||
}
|
||||
|
||||
setValue(settingName, roomId, newValue) {
|
||||
if (!this._cache[settingName]) this._cache[settingName] = {};
|
||||
const bySetting = this._cache[settingName];
|
||||
|
||||
const cacheRoomId = roomId ? roomId : "UNDEFINED"; // avoid weird keys
|
||||
bySetting[cacheRoomId] = newValue;
|
||||
|
||||
const handlerPromise = this._handler.setValue(settingName, roomId, newValue);
|
||||
return Promise.resolve(handlerPromise).finally(() => {
|
||||
delete bySetting[cacheRoomId];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
canSetValue(settingName, roomId) {
|
||||
return this._handler.canSetValue(settingName, roomId);
|
||||
}
|
||||
|
||||
isSupported() {
|
||||
return this._handler.isSupported();
|
||||
}
|
||||
}
|
80
src/settings/handlers/RoomAccountSettingsHandler.js
Normal file
80
src/settings/handlers/RoomAccountSettingsHandler.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
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 SettingsHandler from "./SettingsHandler";
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
|
||||
/**
|
||||
* Gets and sets settings at the "room-account" level for the current user.
|
||||
*/
|
||||
export default class RoomAccountSettingsHandler extends SettingsHandler {
|
||||
getValue(settingName, roomId) {
|
||||
// Special case URL previews
|
||||
if (settingName === "urlPreviewsEnabled") {
|
||||
const content = this._getSettings(roomId, "org.matrix.room.preview_urls");
|
||||
return !content['disable'];
|
||||
}
|
||||
|
||||
// Special case room color
|
||||
if (settingName === "roomColor") {
|
||||
// The event content should already be in an appropriate format, we just need
|
||||
// to get the right value.
|
||||
return this._getSettings(roomId, "org.matrix.room.color_scheme");
|
||||
}
|
||||
|
||||
return this._getSettings(roomId)[settingName];
|
||||
}
|
||||
|
||||
setValue(settingName, roomId, newValue) {
|
||||
// Special case URL previews
|
||||
if (settingName === "urlPreviewsEnabled") {
|
||||
const content = this._getSettings(roomId, "org.matrix.room.preview_urls");
|
||||
content['disable'] = !newValue;
|
||||
return MatrixClientPeg.get().setRoomAccountData(roomId, "org.matrix.room.preview_urls", content);
|
||||
}
|
||||
|
||||
// Special case room color
|
||||
if (settingName === "roomColor") {
|
||||
// The new value should match our requirements, we just need to store it in the right place.
|
||||
return MatrixClientPeg.get().setRoomAccountData(roomId, "org.matrix.room.color_scheme", newValue);
|
||||
}
|
||||
|
||||
const content = this._getSettings(roomId);
|
||||
content[settingName] = newValue;
|
||||
return MatrixClientPeg.get().setRoomAccountData(roomId, "im.vector.web.settings", content);
|
||||
}
|
||||
|
||||
canSetValue(settingName, roomId) {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
|
||||
// If they have the room, they can set their own account data
|
||||
return room !== undefined && room !== null;
|
||||
}
|
||||
|
||||
isSupported() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli !== undefined && cli !== null;
|
||||
}
|
||||
|
||||
_getSettings(roomId, eventType = "im.vector.settings") {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) return {};
|
||||
|
||||
const event = room.getAccountData(eventType);
|
||||
if (!event || !event.getContent()) return {};
|
||||
return event.getContent();
|
||||
}
|
||||
}
|
77
src/settings/handlers/RoomDeviceSettingsHandler.js
Normal file
77
src/settings/handlers/RoomDeviceSettingsHandler.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
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 Promise from 'bluebird';
|
||||
import SettingsHandler from "./SettingsHandler";
|
||||
|
||||
/**
|
||||
* Gets and sets settings at the "room-device" level for the current device in a particular
|
||||
* room.
|
||||
*/
|
||||
export default class RoomDeviceSettingsHandler extends SettingsHandler {
|
||||
getValue(settingName, roomId) {
|
||||
// Special case blacklist setting to use legacy values
|
||||
if (settingName === "blacklistUnverifiedDevices") {
|
||||
const value = this._read("mx_local_settings");
|
||||
if (value && value['blacklistUnverifiedDevicesPerRoom']) {
|
||||
return value['blacklistUnverifiedDevicesPerRoom'][roomId];
|
||||
}
|
||||
}
|
||||
|
||||
const value = this._read(this._getKey(settingName, roomId));
|
||||
if (value) return value.value;
|
||||
return null;
|
||||
}
|
||||
|
||||
setValue(settingName, roomId, newValue) {
|
||||
// Special case blacklist setting for legacy structure
|
||||
if (settingName === "blacklistUnverifiedDevices") {
|
||||
let value = this._read("mx_local_settings");
|
||||
if (!value) value = {};
|
||||
if (!value["blacklistUnverifiedDevicesPerRoom"]) value["blacklistUnverifiedDevicesPerRoom"] = {};
|
||||
value["blacklistUnverifiedDevicesPerRoom"][roomId] = newValue;
|
||||
localStorage.setItem("mx_local_settings", JSON.stringify(value));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (newValue === null) {
|
||||
localStorage.removeItem(this._getKey(settingName, roomId));
|
||||
} else {
|
||||
newValue = JSON.stringify({value: newValue});
|
||||
localStorage.setItem(this._getKey(settingName, roomId), newValue);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
canSetValue(settingName, roomId) {
|
||||
return true; // It's their device, so they should be able to
|
||||
}
|
||||
|
||||
isSupported() {
|
||||
return localStorage !== undefined && localStorage !== null;
|
||||
}
|
||||
|
||||
_read(key) {
|
||||
const rawValue = localStorage.getItem(key);
|
||||
if (!rawValue) return null;
|
||||
return JSON.parse(rawValue);
|
||||
}
|
||||
|
||||
_getKey(settingName, roomId) {
|
||||
return "mx_setting_" + settingName + "_" + roomId;
|
||||
}
|
||||
}
|
70
src/settings/handlers/RoomSettingsHandler.js
Normal file
70
src/settings/handlers/RoomSettingsHandler.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
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 SettingsHandler from "./SettingsHandler";
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
|
||||
/**
|
||||
* Gets and sets settings at the "room" level.
|
||||
*/
|
||||
export default class RoomSettingsHandler extends SettingsHandler {
|
||||
getValue(settingName, roomId) {
|
||||
// Special case URL previews
|
||||
if (settingName === "urlPreviewsEnabled") {
|
||||
const content = this._getSettings(roomId, "org.matrix.room.preview_urls");
|
||||
return !content['disable'];
|
||||
}
|
||||
|
||||
return this._getSettings(roomId)[settingName];
|
||||
}
|
||||
|
||||
setValue(settingName, roomId, newValue) {
|
||||
// Special case URL previews
|
||||
if (settingName === "urlPreviewsEnabled") {
|
||||
const content = this._getSettings(roomId, "org.matrix.room.preview_urls");
|
||||
content['disable'] = !newValue;
|
||||
return MatrixClientPeg.get().sendStateEvent(roomId, "org.matrix.room.preview_urls", content);
|
||||
}
|
||||
|
||||
const content = this._getSettings(roomId);
|
||||
content[settingName] = newValue;
|
||||
return MatrixClientPeg.get().sendStateEvent(roomId, "im.vector.web.settings", content, "");
|
||||
}
|
||||
|
||||
canSetValue(settingName, roomId) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(roomId);
|
||||
|
||||
let eventType = "im.vector.web.settings";
|
||||
if (settingName === "urlPreviewsEnabled") eventType = "org.matrix.room.preview_urls";
|
||||
|
||||
if (!room) return false;
|
||||
return room.currentState.maySendStateEvent(eventType, cli.getUserId());
|
||||
}
|
||||
|
||||
isSupported() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli !== undefined && cli !== null;
|
||||
}
|
||||
|
||||
_getSettings(roomId, eventType = "im.vector.web.settings") {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) return {};
|
||||
const event = room.currentState.getStateEvents(eventType, "");
|
||||
if (!event || !event.getContent()) return {};
|
||||
return event.getContent();
|
||||
}
|
||||
}
|
71
src/settings/handlers/SettingsHandler.js
Normal file
71
src/settings/handlers/SettingsHandler.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
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 Promise from "bluebird";
|
||||
|
||||
/**
|
||||
* Represents the base class for all level handlers. This class performs no logic
|
||||
* and should be overridden.
|
||||
*/
|
||||
export default class SettingsHandler {
|
||||
/**
|
||||
* Gets the value for a particular setting at this level for a particular room.
|
||||
* If no room is applicable, the roomId may be null. The roomId may not be
|
||||
* applicable to this level and may be ignored by the handler.
|
||||
* @param {string} settingName The name of the setting.
|
||||
* @param {String} roomId The room ID to read from, may be null.
|
||||
* @returns {*} The setting value, or null if not found.
|
||||
*/
|
||||
getValue(settingName, roomId) {
|
||||
console.error("Invalid operation: getValue was not overridden");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value for a particular setting at this level for a particular room.
|
||||
* If no room is applicable, the roomId may be null. The roomId may not be
|
||||
* applicable to this level and may be ignored by the handler. Setting a value
|
||||
* to null will cause the level to remove the value. The current user should be
|
||||
* able to set the value prior to calling this.
|
||||
* @param {string} settingName The name of the setting to change.
|
||||
* @param {String} roomId The room ID to set the value in, may be null.
|
||||
* @param {*} newValue The new value for the setting, may be null.
|
||||
* @returns {Promise} Resolves when the setting has been saved.
|
||||
*/
|
||||
setValue(settingName, roomId, newValue) {
|
||||
console.error("Invalid operation: setValue was not overridden");
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current user is able to set the value of the given setting
|
||||
* in the given room at this level.
|
||||
* @param {string} settingName The name of the setting to check.
|
||||
* @param {String} roomId The room ID to check in, may be null
|
||||
* @returns {boolean} True if the setting can be set by the user, false otherwise.
|
||||
*/
|
||||
canSetValue(settingName, roomId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this level is supported on this device.
|
||||
* @returns {boolean} True if this level is supported on the current device.
|
||||
*/
|
||||
isSupported() {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -14,6 +14,8 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
|
||||
function memberEventDiff(ev) {
|
||||
const diff = {
|
||||
isMemberEvent: ev.getType() === 'm.room.member',
|
||||
|
@ -34,16 +36,19 @@ function memberEventDiff(ev) {
|
|||
return diff;
|
||||
}
|
||||
|
||||
export default function shouldHideEvent(ev, syncedSettings) {
|
||||
export default function shouldHideEvent(ev) {
|
||||
// Wrap getValue() for readability
|
||||
const isEnabled = (name) => SettingsStore.getValue(name, ev.getRoomId());
|
||||
|
||||
// Hide redacted events
|
||||
if (syncedSettings['hideRedactions'] && ev.isRedacted()) return true;
|
||||
if (isEnabled('hideRedactions') && ev.isRedacted()) return true;
|
||||
|
||||
const eventDiff = memberEventDiff(ev);
|
||||
|
||||
if (eventDiff.isMemberEvent) {
|
||||
if (syncedSettings['hideJoinLeaves'] && (eventDiff.isJoin || eventDiff.isPart)) return true;
|
||||
const isMemberAvatarDisplaynameChange = eventDiff.isAvatarChange || eventDiff.isDisplaynameChange;
|
||||
if (syncedSettings['hideAvatarDisplaynameChanges'] && isMemberAvatarDisplaynameChange) return true;
|
||||
if (isEnabled('hideJoinLeaves') && (eventDiff.isJoin || eventDiff.isPart)) return true;
|
||||
if (isEnabled('hideAvatarChanges') && eventDiff.isAvatarChange) return true;
|
||||
if (isEnabled('hideDisplaynameChanges') && eventDiff.isDisplaynameChange) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue