Merge pull request #4424 from JorikSchellekens/joriks/font-scaling-slider
Font scaling settings and slider
This commit is contained in:
commit
d95d0191a2
28 changed files with 764 additions and 202 deletions
|
@ -115,6 +115,7 @@
|
||||||
@import "./views/elements/_RichText.scss";
|
@import "./views/elements/_RichText.scss";
|
||||||
@import "./views/elements/_RoleButton.scss";
|
@import "./views/elements/_RoleButton.scss";
|
||||||
@import "./views/elements/_RoomAliasField.scss";
|
@import "./views/elements/_RoomAliasField.scss";
|
||||||
|
@import "./views/elements/_Slider.scss";
|
||||||
@import "./views/elements/_Spinner.scss";
|
@import "./views/elements/_Spinner.scss";
|
||||||
@import "./views/elements/_SyntaxHighlight.scss";
|
@import "./views/elements/_SyntaxHighlight.scss";
|
||||||
@import "./views/elements/_TextWithTooltip.scss";
|
@import "./views/elements/_TextWithTooltip.scss";
|
||||||
|
@ -206,6 +207,7 @@
|
||||||
@import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss";
|
@import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss";
|
||||||
@import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss";
|
@import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss";
|
||||||
@import "./views/settings/tabs/room/_SecurityRoomSettingsTab.scss";
|
@import "./views/settings/tabs/room/_SecurityRoomSettingsTab.scss";
|
||||||
|
@import "./views/settings/tabs/user/_AppearanceUserSettingsTab.scss";
|
||||||
@import "./views/settings/tabs/user/_GeneralUserSettingsTab.scss";
|
@import "./views/settings/tabs/user/_GeneralUserSettingsTab.scss";
|
||||||
@import "./views/settings/tabs/user/_HelpUserSettingsTab.scss";
|
@import "./views/settings/tabs/user/_HelpUserSettingsTab.scss";
|
||||||
@import "./views/settings/tabs/user/_MjolnirUserSettingsTab.scss";
|
@import "./views/settings/tabs/user/_MjolnirUserSettingsTab.scss";
|
||||||
|
|
|
@ -69,7 +69,7 @@ limitations under the License.
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.mx_TagPanel .mx_TagPanel_tagTileContainer > div {
|
.mx_TagPanel .mx_TagPanel_tagTileContainer > div {
|
||||||
height: $font-40px;
|
height: 40px;
|
||||||
padding: 10px 0 9px 0;
|
padding: 10px 0 9px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ limitations under the License.
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -15px;
|
left: -15px;
|
||||||
border-radius: 0 3px 3px 0;
|
border-radius: 0 3px 3px 0;
|
||||||
top: -8px; // (16px / 2)
|
top: -8px; // (16px from height / 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus {
|
.mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus {
|
||||||
|
|
|
@ -43,7 +43,7 @@ limitations under the License.
|
||||||
margin: 0 7px;
|
margin: 0 7px;
|
||||||
mask: url('$(res)/img/feather-customised/dropdown-arrow.svg');
|
mask: url('$(res)/img/feather-customised/dropdown-arrow.svg');
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
width: 10px;
|
width: $font-22px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
background-color: $roomsublist-label-fg-color;
|
background-color: $roomsublist-label-fg-color;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,10 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/feather-customised/settings.svg');
|
mask-image: url('$(res)/img/feather-customised/settings.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_UserSettingsDialog_appearanceIcon::before {
|
||||||
|
mask-image: url('$(res)/img/feather-customised/brush.svg');
|
||||||
|
}
|
||||||
|
|
||||||
.mx_UserSettingsDialog_voiceIcon::before {
|
.mx_UserSettingsDialog_voiceIcon::before {
|
||||||
mask-image: url('$(res)/img/feather-customised/phone.svg');
|
mask-image: url('$(res)/img/feather-customised/phone.svg');
|
||||||
}
|
}
|
||||||
|
|
99
res/css/views/elements/_Slider.scss
Normal file
99
res/css/views/elements/_Slider.scss
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_Slider {
|
||||||
|
position: relative;
|
||||||
|
margin: 0px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Slider_dotContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Slider_bar {
|
||||||
|
display: flex;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
height: 1em;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 0.5em; // half the width of a dot.
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Slider_bar > hr {
|
||||||
|
width: 100%;
|
||||||
|
height: 0.4em;
|
||||||
|
background-color: $slider-background-color;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Slider_selection {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: calc(100% - 1em); // 2 * half the width of a dot
|
||||||
|
height: 1em;
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Slider_selectionDot {
|
||||||
|
position: absolute;
|
||||||
|
width: 1.1em;
|
||||||
|
height: 1.1em;
|
||||||
|
background-color: $slider-selection-color;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 6px lightgrey;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Slider_selection > hr {
|
||||||
|
margin: 0;
|
||||||
|
border: 0.2em solid $slider-selection-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Slider_dot {
|
||||||
|
height: 1em;
|
||||||
|
width: 1em;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: $slider-background-color;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Slider_dotActive {
|
||||||
|
background-color: $slider-selection-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Slider_dotValue {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
color: $slider-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following is a hack to center the labels without adding
|
||||||
|
// any width to the slider's dots.
|
||||||
|
.mx_Slider_labelContainer {
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Slider_label {
|
||||||
|
position: relative;
|
||||||
|
width: fit-content;
|
||||||
|
left: -50%;
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ limitations under the License.
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: $font-34px;
|
height: 32px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 8px 0 10px;
|
padding: 0 8px 0 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -81,6 +81,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_RoomTile_avatar_container {
|
.mx_RoomTile_avatar_container {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomTile_avatar {
|
.mx_RoomTile_avatar {
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_AppearanceUserSettingsTab_fontSlider,
|
||||||
|
.mx_AppearanceUserSettingsTab_themeSection .mx_Field,
|
||||||
|
.mx_AppearanceUserSettingsTab_fontScaling .mx_Field {
|
||||||
|
@mixin mx_Settings_fullWidthField;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AppearanceUserSettingsTab_fontSlider {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px;
|
||||||
|
background: $font-slider-bg-color;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AppearanceUserSettingsTab_fontSlider_smallText {
|
||||||
|
font-size: 15px;
|
||||||
|
padding-right: 20px;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AppearanceUserSettingsTab_fontSlider_largeText {
|
||||||
|
font-size: 18px;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
|
@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_GeneralUserSettingsTab_changePassword .mx_Field,
|
.mx_GeneralUserSettingsTab_changePassword .mx_Field {
|
||||||
.mx_GeneralUserSettingsTab_themeSection .mx_Field {
|
|
||||||
@mixin mx_Settings_fullWidthField;
|
@mixin mx_Settings_fullWidthField;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
5
res/img/feather-customised/brush.svg
Normal file
5
res/img/feather-customised/brush.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 16.5C12 18.9853 9.98528 21 7.5 21C6.21514 21 3 21 3 21C3 21 3 17.7004 3 16.5C3 14.0147 5.01472 12 7.5 12C9.98528 12 12 14.0147 12 16.5Z" stroke="#2E2F32" stroke-linejoin="round"/>
|
||||||
|
<path d="M8.25 12L17.1955 3.69345C18.0632 2.88776 19.4127 2.91274 20.25 3.75V3.75C21.0873 4.58726 21.1122 5.93682 20.3065 6.80449L12 15.75" stroke="#2E2F32"/>
|
||||||
|
<path d="M11.25 9C11.25 9 12.3929 9.45 13.5 10.5C14.6071 11.55 15 12.75 15 12.75" stroke="#2E2F32"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 556 B |
|
@ -180,6 +180,9 @@ $breadcrumb-placeholder-bg-color: #272c35;
|
||||||
|
|
||||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
|
// FontSlider colors
|
||||||
|
$font-slider-bg-color: $room-highlight-color;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
|
|
@ -262,6 +262,10 @@ $togglesw-off-color: #c1c9d6;
|
||||||
$togglesw-on-color: $accent-color;
|
$togglesw-on-color: $accent-color;
|
||||||
$togglesw-ball-color: #fff;
|
$togglesw-ball-color: #fff;
|
||||||
|
|
||||||
|
// Slider
|
||||||
|
$slider-selection-color: $accent-color;
|
||||||
|
$slider-background-color: #c1c9d6;
|
||||||
|
|
||||||
$progressbar-color: #000;
|
$progressbar-color: #000;
|
||||||
|
|
||||||
$room-warning-bg-color: $yellow-background;
|
$room-warning-bg-color: $yellow-background;
|
||||||
|
@ -302,6 +306,9 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
|
||||||
|
|
||||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
|
// FontSlider colors
|
||||||
|
$font-slider-bg-color: rgba($input-darker-bg-color, 0.2);
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
|
51
src/FontWatcher.js
Normal file
51
src/FontWatcher.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import dis from './dispatcher/dispatcher';
|
||||||
|
import SettingsStore, {SettingLevel} from './settings/SettingsStore';
|
||||||
|
|
||||||
|
export class FontWatcher {
|
||||||
|
static MIN_SIZE = 13;
|
||||||
|
static MAX_SIZE = 20;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._dispatcherRef = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this._setRootFontSize(SettingsStore.getValue("fontSize"));
|
||||||
|
this._dispatcherRef = dis.register(this._onAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
dis.unregister(this._dispatcherRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onAction = (payload) => {
|
||||||
|
if (payload.action === 'update-font-size') {
|
||||||
|
this._setRootFontSize(payload.size);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_setRootFontSize = (size) => {
|
||||||
|
const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE);
|
||||||
|
|
||||||
|
if (fontSize != size) {
|
||||||
|
SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize);
|
||||||
|
}
|
||||||
|
document.querySelector(":root").style.fontSize = fontSize + "px";
|
||||||
|
};
|
||||||
|
}
|
|
@ -60,6 +60,7 @@ import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDisco
|
||||||
import DMRoomMap from '../../utils/DMRoomMap';
|
import DMRoomMap from '../../utils/DMRoomMap';
|
||||||
import { countRoomsWithNotif } from '../../RoomNotifs';
|
import { countRoomsWithNotif } from '../../RoomNotifs';
|
||||||
import { ThemeWatcher } from "../../theme";
|
import { ThemeWatcher } from "../../theme";
|
||||||
|
import { FontWatcher } from '../../FontWatcher';
|
||||||
import { storeRoomAliasInCache } from '../../RoomAliasCache';
|
import { storeRoomAliasInCache } from '../../RoomAliasCache';
|
||||||
import { defer, IDeferred } from "../../utils/promise";
|
import { defer, IDeferred } from "../../utils/promise";
|
||||||
import ToastStore from "../../stores/ToastStore";
|
import ToastStore from "../../stores/ToastStore";
|
||||||
|
@ -216,6 +217,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
private readonly loggedInView: React.RefObject<LoggedInViewType>;
|
private readonly loggedInView: React.RefObject<LoggedInViewType>;
|
||||||
private readonly dispatcherRef: any;
|
private readonly dispatcherRef: any;
|
||||||
private readonly themeWatcher: ThemeWatcher;
|
private readonly themeWatcher: ThemeWatcher;
|
||||||
|
private readonly fontWatcher: FontWatcher;
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -283,8 +285,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
this.accountPasswordTimer = null;
|
this.accountPasswordTimer = null;
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
|
||||||
this.themeWatcher = new ThemeWatcher();
|
this.themeWatcher = new ThemeWatcher();
|
||||||
|
this.fontWatcher = new FontWatcher();
|
||||||
this.themeWatcher.start();
|
this.themeWatcher.start();
|
||||||
|
this.fontWatcher.start();
|
||||||
|
|
||||||
this.focusComposer = false;
|
this.focusComposer = false;
|
||||||
|
|
||||||
|
@ -367,6 +372,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
Lifecycle.stopMatrixClient();
|
Lifecycle.stopMatrixClient();
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
this.themeWatcher.stop();
|
this.themeWatcher.stop();
|
||||||
|
this.fontWatcher.stop();
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
|
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ import RoomTile from "../views/rooms/RoomTile";
|
||||||
import LazyRenderList from "../views/elements/LazyRenderList";
|
import LazyRenderList from "../views/elements/LazyRenderList";
|
||||||
import {_t} from "../../languageHandler";
|
import {_t} from "../../languageHandler";
|
||||||
import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex";
|
import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex";
|
||||||
import toRem from "../../utils/rem";
|
import {toPx} from "../../utils/units";
|
||||||
|
|
||||||
// turn this on for drop & drag console debugging galore
|
// turn this on for drop & drag console debugging galore
|
||||||
const debug = false;
|
const debug = false;
|
||||||
|
@ -420,7 +420,7 @@ export default class RoomSubList extends React.PureComponent {
|
||||||
|
|
||||||
setHeight = (height) => {
|
setHeight = (height) => {
|
||||||
if (this._subList.current) {
|
if (this._subList.current) {
|
||||||
this._subList.current.style.height = toRem(height);
|
this._subList.current.style.height = toPx(height);
|
||||||
}
|
}
|
||||||
this._updateLazyRenderHeight(height);
|
this._updateLazyRenderHeight(height);
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,7 +24,7 @@ import * as AvatarLogic from '../../../Avatar';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import toRem from "../../../utils/rem";
|
import {toPx} from "../../../utils/units";
|
||||||
|
|
||||||
export default createReactClass({
|
export default createReactClass({
|
||||||
displayName: 'BaseAvatar',
|
displayName: 'BaseAvatar',
|
||||||
|
@ -166,9 +166,9 @@ export default createReactClass({
|
||||||
const textNode = (
|
const textNode = (
|
||||||
<span className="mx_BaseAvatar_initial" aria-hidden="true"
|
<span className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||||
style={{
|
style={{
|
||||||
fontSize: toRem(width * 0.65),
|
fontSize: toPx(width * 0.65),
|
||||||
width: toRem(width),
|
width: toPx(width),
|
||||||
lineHeight: toRem(height),
|
lineHeight: toPx(height),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{ initialLetter }
|
{ initialLetter }
|
||||||
|
@ -179,8 +179,8 @@ export default createReactClass({
|
||||||
alt="" title={title} onError={this.onError}
|
alt="" title={title} onError={this.onError}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
style={{
|
style={{
|
||||||
width: toRem(width),
|
width: toPx(width),
|
||||||
height: toRem(height)
|
height: toPx(height)
|
||||||
}} />
|
}} />
|
||||||
);
|
);
|
||||||
if (onClick != null) {
|
if (onClick != null) {
|
||||||
|
@ -210,8 +210,8 @@ export default createReactClass({
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onError={this.onError}
|
onError={this.onError}
|
||||||
style={{
|
style={{
|
||||||
width: toRem(width),
|
width: toPx(width),
|
||||||
height: toRem(height),
|
height: toPx(height),
|
||||||
}}
|
}}
|
||||||
title={title} alt=""
|
title={title} alt=""
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
|
@ -224,8 +224,8 @@ export default createReactClass({
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
onError={this.onError}
|
onError={this.onError}
|
||||||
style={{
|
style={{
|
||||||
width: toRem(width),
|
width: toPx(width),
|
||||||
height: toRem(height),
|
height: toPx(height),
|
||||||
}}
|
}}
|
||||||
title={title} alt=""
|
title={title} alt=""
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {_t, _td} from "../../../languageHandler";
|
||||||
import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab";
|
import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab";
|
import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab";
|
||||||
|
import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab";
|
||||||
import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab";
|
import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab";
|
||||||
import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserSettingsTab";
|
import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserSettingsTab";
|
||||||
import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab";
|
import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab";
|
||||||
|
@ -66,6 +67,11 @@ export default class UserSettingsDialog extends React.Component {
|
||||||
"mx_UserSettingsDialog_settingsIcon",
|
"mx_UserSettingsDialog_settingsIcon",
|
||||||
<GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
<GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||||
));
|
));
|
||||||
|
tabs.push(new Tab(
|
||||||
|
_td("Appearance"),
|
||||||
|
"mx_UserSettingsDialog_appearanceIcon",
|
||||||
|
<AppearanceUserSettingsTab />,
|
||||||
|
));
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
_td("Flair"),
|
_td("Flair"),
|
||||||
"mx_UserSettingsDialog_flairIcon",
|
"mx_UserSettingsDialog_flairIcon",
|
||||||
|
|
146
src/components/views/elements/Slider.tsx
Normal file
146
src/components/views/elements/Slider.tsx
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 * as React from 'react';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// A callback for the selected value
|
||||||
|
onSelectionChange: (value: number) => void;
|
||||||
|
|
||||||
|
// The current value of the slider
|
||||||
|
value: number;
|
||||||
|
|
||||||
|
// The range and values of the slider
|
||||||
|
// Currently only supports an ascending, constant interval range
|
||||||
|
values: number[];
|
||||||
|
|
||||||
|
// A function for formatting the the values
|
||||||
|
displayFunc: (value: number) => string;
|
||||||
|
|
||||||
|
// Whether the slider is disabled
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Slider extends React.Component<IProps> {
|
||||||
|
// offset is a terrible inverse approximation.
|
||||||
|
// if the values represents some function f(x) = y where x is the
|
||||||
|
// index of the array and y = values[x] then offset(f, y) = x
|
||||||
|
// s.t f(x) = y.
|
||||||
|
// it assumes a monotonic function and interpolates linearly between
|
||||||
|
// y values.
|
||||||
|
// Offset is used for finding the location of a value on a
|
||||||
|
// non linear slider.
|
||||||
|
private offset(values: number[], value: number): number {
|
||||||
|
// the index of the first number greater than value.
|
||||||
|
let closest = values.reduce((prev, curr) => {
|
||||||
|
return (value > curr ? prev + 1 : prev);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Off the left
|
||||||
|
if (closest === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Off the right
|
||||||
|
if (closest === values.length) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now
|
||||||
|
const closestLessValue = values[closest - 1];
|
||||||
|
const closestGreaterValue = values[closest];
|
||||||
|
|
||||||
|
const intervalWidth = 1 / (values.length - 1);
|
||||||
|
|
||||||
|
const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue)
|
||||||
|
|
||||||
|
return 100 * (closest - 1 + linearInterpolation) * intervalWidth
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): React.ReactNode {
|
||||||
|
const dots = this.props.values.map(v =>
|
||||||
|
<Dot active={v <= this.props.value}
|
||||||
|
label={this.props.displayFunc(v)}
|
||||||
|
onClick={this.props.disabled ? () => {} : () => this.props.onSelectionChange(v)}
|
||||||
|
key={v}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
/>);
|
||||||
|
|
||||||
|
let selection = null;
|
||||||
|
|
||||||
|
if (!this.props.disabled) {
|
||||||
|
const offset = this.offset(this.props.values, this.props.value);
|
||||||
|
selection = <div className="mx_Slider_selection">
|
||||||
|
<div className="mx_Slider_selectionDot" style={{left: "calc(-0.55em + " + offset + "%)"}} />
|
||||||
|
<hr style={{width: offset + "%"}} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_Slider">
|
||||||
|
<div>
|
||||||
|
<div className="mx_Slider_bar">
|
||||||
|
<hr onClick={this.props.disabled ? () => {} : this.onClick.bind(this)}/>
|
||||||
|
{ selection }
|
||||||
|
</div>
|
||||||
|
<div className="mx_Slider_dotContainer">
|
||||||
|
{dots}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick(event: React.MouseEvent) {
|
||||||
|
const width = (event.target as HTMLElement).clientWidth;
|
||||||
|
// nativeEvent is safe to use because https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/offsetX
|
||||||
|
// is supported by all modern browsers
|
||||||
|
const relativeClick = (event.nativeEvent.offsetX / width);
|
||||||
|
const nearestValue = this.props.values[Math.round(relativeClick * (this.props.values.length - 1))];
|
||||||
|
this.props.onSelectionChange(nearestValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDotProps {
|
||||||
|
// Callback for behavior onclick
|
||||||
|
onClick: () => void,
|
||||||
|
|
||||||
|
// Whether the dot should appear active
|
||||||
|
active: boolean,
|
||||||
|
|
||||||
|
// The label on the dot
|
||||||
|
label: string,
|
||||||
|
|
||||||
|
// Whether the slider is disabled
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dot extends React.PureComponent<IDotProps> {
|
||||||
|
render(): React.ReactNode {
|
||||||
|
let className = "mx_Slider_dot"
|
||||||
|
if (!this.props.disabled && this.props.active) {
|
||||||
|
className += " mx_Slider_dotActive";
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span onClick={this.props.onClick} className="mx_Slider_dotValue">
|
||||||
|
<div className={className} />
|
||||||
|
<div className="mx_Slider_labelContainer">
|
||||||
|
<div className="mx_Slider_label">
|
||||||
|
{this.props.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,7 +34,7 @@ import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
|
||||||
import * as ObjectUtils from "../../../ObjectUtils";
|
import * as ObjectUtils from "../../../ObjectUtils";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {E2E_STATE} from "./E2EIcon";
|
import {E2E_STATE} from "./E2EIcon";
|
||||||
import toRem from "../../../utils/rem";
|
import {toRem} from "../../../utils/units";
|
||||||
|
|
||||||
const eventTileTypes = {
|
const eventTileTypes = {
|
||||||
'm.room.message': 'messages.MessageEvent',
|
'm.room.message': 'messages.MessageEvent',
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { _t } from '../../../languageHandler';
|
||||||
import {formatDate} from '../../../DateUtils';
|
import {formatDate} from '../../../DateUtils';
|
||||||
import Velociraptor from "../../../Velociraptor";
|
import Velociraptor from "../../../Velociraptor";
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import toRem from "../../../utils/rem";
|
import {toRem} from "../../../utils/units";
|
||||||
|
|
||||||
let bounce = false;
|
let bounce = false;
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -0,0 +1,281 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 New Vector Ltd
|
||||||
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
|
||||||
|
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 {_t} from "../../../../../languageHandler";
|
||||||
|
import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore";
|
||||||
|
import * as sdk from "../../../../../index";
|
||||||
|
import {enumerateThemes, ThemeWatcher} from "../../../../../theme";
|
||||||
|
import Field from "../../../elements/Field";
|
||||||
|
import Slider from "../../../elements/Slider";
|
||||||
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
|
import dis from "../../../../../dispatcher/dispatcher";
|
||||||
|
import { FontWatcher } from "../../../../../FontWatcher";
|
||||||
|
|
||||||
|
export default class AppearanceUserSettingsTab extends React.Component {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
fontSize: SettingsStore.getValue("fontSize", null),
|
||||||
|
...this._calculateThemeState(),
|
||||||
|
customThemeUrl: "",
|
||||||
|
customThemeMessage: {isError: false, text: ""},
|
||||||
|
useCustomFontSize: SettingsStore.getValue("useCustomFontSize"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_calculateThemeState() {
|
||||||
|
// We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
|
||||||
|
// show the right values for things.
|
||||||
|
|
||||||
|
const themeChoice = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme");
|
||||||
|
const systemThemeExplicit = SettingsStore.getValueAt(
|
||||||
|
SettingLevel.DEVICE, "use_system_theme", null, false, true);
|
||||||
|
const themeExplicit = SettingsStore.getValueAt(
|
||||||
|
SettingLevel.DEVICE, "theme", null, false, true);
|
||||||
|
|
||||||
|
// If the user has enabled system theme matching, use that.
|
||||||
|
if (systemThemeExplicit) {
|
||||||
|
return {
|
||||||
|
theme: themeChoice,
|
||||||
|
useSystemTheme: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user has set a theme explicitly, use that (no system theme matching)
|
||||||
|
if (themeExplicit) {
|
||||||
|
return {
|
||||||
|
theme: themeChoice,
|
||||||
|
useSystemTheme: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise assume the defaults for the settings
|
||||||
|
return {
|
||||||
|
theme: themeChoice,
|
||||||
|
useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_onThemeChange = (e) => {
|
||||||
|
const newTheme = e.target.value;
|
||||||
|
if (this.state.theme === newTheme) return;
|
||||||
|
|
||||||
|
// doing getValue in the .catch will still return the value we failed to set,
|
||||||
|
// so remember what the value was before we tried to set it so we can revert
|
||||||
|
const oldTheme = SettingsStore.getValue('theme');
|
||||||
|
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => {
|
||||||
|
dis.dispatch({action: 'recheck_theme'});
|
||||||
|
this.setState({theme: oldTheme});
|
||||||
|
});
|
||||||
|
this.setState({theme: newTheme});
|
||||||
|
// The settings watcher doesn't fire until the echo comes back from the
|
||||||
|
// server, so to make the theme change immediately we need to manually
|
||||||
|
// do the dispatch now
|
||||||
|
// XXX: The local echoed value appears to be unreliable, in particular
|
||||||
|
// when settings custom themes(!) so adding forceTheme to override
|
||||||
|
// the value from settings.
|
||||||
|
dis.dispatch({action: 'recheck_theme', forceTheme: newTheme});
|
||||||
|
};
|
||||||
|
|
||||||
|
_onUseSystemThemeChanged = (checked) => {
|
||||||
|
this.setState({useSystemTheme: checked});
|
||||||
|
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked);
|
||||||
|
dis.dispatch({action: 'recheck_theme'});
|
||||||
|
};
|
||||||
|
|
||||||
|
_onFontSizeChanged = (size) => {
|
||||||
|
this.setState({fontSize: size});
|
||||||
|
SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, size);
|
||||||
|
};
|
||||||
|
|
||||||
|
_onValidateFontSize = ({value}) => {
|
||||||
|
console.log({value});
|
||||||
|
|
||||||
|
const parsedSize = parseFloat(value);
|
||||||
|
const min = FontWatcher.MIN_SIZE;
|
||||||
|
const max = FontWatcher.MAX_SIZE;
|
||||||
|
|
||||||
|
if (isNaN(parsedSize)) {
|
||||||
|
return {valid: false, feedback: _t("Size must be a number")};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(min <= parsedSize && parsedSize <= max)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
feedback: _t('Custom font size can only be between %(min)s pt and %(max)s pt', {min, max}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, value);
|
||||||
|
return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})};
|
||||||
|
}
|
||||||
|
|
||||||
|
_onAddCustomTheme = async () => {
|
||||||
|
let currentThemes = SettingsStore.getValue("custom_themes");
|
||||||
|
if (!currentThemes) currentThemes = [];
|
||||||
|
currentThemes = currentThemes.map(c => c); // cheap clone
|
||||||
|
|
||||||
|
if (this._themeTimer) {
|
||||||
|
clearTimeout(this._themeTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch(this.state.customThemeUrl);
|
||||||
|
const themeInfo = await r.json();
|
||||||
|
if (!themeInfo || typeof(themeInfo['name']) !== 'string' || typeof(themeInfo['colors']) !== 'object') {
|
||||||
|
this.setState({customThemeMessage: {text: _t("Invalid theme schema."), isError: true}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentThemes.push(themeInfo);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.setState({customThemeMessage: {text: _t("Error downloading theme information."), isError: true}});
|
||||||
|
return; // Don't continue on error
|
||||||
|
}
|
||||||
|
|
||||||
|
await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes);
|
||||||
|
this.setState({customThemeUrl: "", customThemeMessage: {text: _t("Theme added!"), isError: false}});
|
||||||
|
|
||||||
|
this._themeTimer = setTimeout(() => {
|
||||||
|
this.setState({customThemeMessage: {text: "", isError: false}});
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
_onCustomThemeChange = (e) => {
|
||||||
|
this.setState({customThemeUrl: e.target.value});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="mx_SettingsTab">
|
||||||
|
<div className="mx_SettingsTab_heading">{_t("Appearance")}</div>
|
||||||
|
{this._renderThemeSection()}
|
||||||
|
{SettingsStore.isFeatureEnabled("feature_font_scaling") ? this._renderFontSection() : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderThemeSection() {
|
||||||
|
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
|
||||||
|
const LabelledToggleSwitch = sdk.getComponent("views.elements.LabelledToggleSwitch");
|
||||||
|
|
||||||
|
const themeWatcher = new ThemeWatcher();
|
||||||
|
let systemThemeSection;
|
||||||
|
if (themeWatcher.isSystemThemeSupported()) {
|
||||||
|
systemThemeSection = <div>
|
||||||
|
<LabelledToggleSwitch
|
||||||
|
value={this.state.useSystemTheme}
|
||||||
|
label={SettingsStore.getDisplayName("use_system_theme")}
|
||||||
|
onChange={this._onUseSystemThemeChanged}
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let customThemeForm;
|
||||||
|
if (SettingsStore.isFeatureEnabled("feature_custom_themes")) {
|
||||||
|
let messageElement = null;
|
||||||
|
if (this.state.customThemeMessage.text) {
|
||||||
|
if (this.state.customThemeMessage.isError) {
|
||||||
|
messageElement = <div className='text-error'>{this.state.customThemeMessage.text}</div>;
|
||||||
|
} else {
|
||||||
|
messageElement = <div className='text-success'>{this.state.customThemeMessage.text}</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customThemeForm = (
|
||||||
|
<div className='mx_SettingsTab_section'>
|
||||||
|
<form onSubmit={this._onAddCustomTheme}>
|
||||||
|
<Field
|
||||||
|
label={_t("Custom theme URL")}
|
||||||
|
type='text'
|
||||||
|
id='mx_GeneralUserSettingsTab_customThemeInput'
|
||||||
|
autoComplete="off"
|
||||||
|
onChange={this._onCustomThemeChange}
|
||||||
|
value={this.state.customThemeUrl}
|
||||||
|
/>
|
||||||
|
<AccessibleButton
|
||||||
|
onClick={this._onAddCustomTheme}
|
||||||
|
type="submit" kind="primary_sm"
|
||||||
|
disabled={!this.state.customThemeUrl.trim()}
|
||||||
|
>{_t("Add theme")}</AccessibleButton>
|
||||||
|
{messageElement}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const themes = Object.entries(enumerateThemes())
|
||||||
|
.map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability
|
||||||
|
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
|
||||||
|
const customThemes = themes.filter(p => !builtInThemes.includes(p))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
const orderedThemes = [...builtInThemes, ...customThemes];
|
||||||
|
return (
|
||||||
|
<div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_themeSection">
|
||||||
|
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
|
||||||
|
{systemThemeSection}
|
||||||
|
<Field
|
||||||
|
id="theme" label={_t("Theme")} element="select"
|
||||||
|
value={this.state.theme} onChange={this._onThemeChange}
|
||||||
|
disabled={this.state.useSystemTheme}
|
||||||
|
>
|
||||||
|
{orderedThemes.map(theme => {
|
||||||
|
return <option key={theme.id} value={theme.id}>{theme.name}</option>;
|
||||||
|
})}
|
||||||
|
</Field>
|
||||||
|
{customThemeForm}
|
||||||
|
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderFontSection() {
|
||||||
|
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
|
||||||
|
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_fontScaling">
|
||||||
|
<span className="mx_SettingsTab_subheading">{_t("Font size")}</span>
|
||||||
|
<div className="mx_AppearanceUserSettingsTab_fontSlider">
|
||||||
|
<div className="mx_AppearanceUserSettingsTab_fontSlider_smallText">Aa</div>
|
||||||
|
<Slider
|
||||||
|
values={[13, 15, 16, 18, 20]}
|
||||||
|
value={this.state.fontSize}
|
||||||
|
onSelectionChange={this._onFontSizeChanged}
|
||||||
|
displayFunc={value => {}}
|
||||||
|
disabled={this.state.useCustomFontSize}
|
||||||
|
/>
|
||||||
|
<div className="mx_AppearanceUserSettingsTab_fontSlider_largeText">Aa</div>
|
||||||
|
</div>
|
||||||
|
<SettingsFlag
|
||||||
|
name="useCustomFontSize"
|
||||||
|
level={SettingLevel.ACCOUNT}
|
||||||
|
onChange={(checked)=> this.setState({useCustomFontSize: checked})}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
label={_t("Font size")}
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder={this.state.fontSize.toString()}
|
||||||
|
value={this.state.fontSize.toString()}
|
||||||
|
id="font_size_field"
|
||||||
|
onValidate={this._onValidateFontSize}
|
||||||
|
onChange={(value) => this.setState({fontSize: value.target.value})}
|
||||||
|
disabled={!this.state.useCustomFontSize}
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,6 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {_t} from "../../../../../languageHandler";
|
import {_t} from "../../../../../languageHandler";
|
||||||
import ProfileSettings from "../../ProfileSettings";
|
import ProfileSettings from "../../ProfileSettings";
|
||||||
import Field from "../../../elements/Field";
|
|
||||||
import * as languageHandler from "../../../../../languageHandler";
|
import * as languageHandler from "../../../../../languageHandler";
|
||||||
import {SettingLevel} from "../../../../../settings/SettingsStore";
|
import {SettingLevel} from "../../../../../settings/SettingsStore";
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
|
@ -27,7 +26,6 @@ import LanguageDropdown from "../../../elements/LanguageDropdown";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
|
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import {enumerateThemes, ThemeWatcher} from "../../../../../theme";
|
|
||||||
import PlatformPeg from "../../../../../PlatformPeg";
|
import PlatformPeg from "../../../../../PlatformPeg";
|
||||||
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
|
||||||
import * as sdk from "../../../../..";
|
import * as sdk from "../../../../..";
|
||||||
|
@ -62,9 +60,6 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
emails: [],
|
emails: [],
|
||||||
msisdns: [],
|
msisdns: [],
|
||||||
loading3pids: true, // whether or not the emails and msisdns have been loaded
|
loading3pids: true, // whether or not the emails and msisdns have been loaded
|
||||||
...this._calculateThemeState(),
|
|
||||||
customThemeUrl: "",
|
|
||||||
customThemeMessage: {isError: false, text: ""},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this._onAction);
|
this.dispatcherRef = dis.register(this._onAction);
|
||||||
|
@ -93,39 +88,6 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
_calculateThemeState() {
|
|
||||||
// We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
|
|
||||||
// show the right values for things.
|
|
||||||
|
|
||||||
const themeChoice = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme");
|
|
||||||
const systemThemeExplicit = SettingsStore.getValueAt(
|
|
||||||
SettingLevel.DEVICE, "use_system_theme", null, false, true);
|
|
||||||
const themeExplicit = SettingsStore.getValueAt(
|
|
||||||
SettingLevel.DEVICE, "theme", null, false, true);
|
|
||||||
|
|
||||||
// If the user has enabled system theme matching, use that.
|
|
||||||
if (systemThemeExplicit) {
|
|
||||||
return {
|
|
||||||
theme: themeChoice,
|
|
||||||
useSystemTheme: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the user has set a theme explicitly, use that (no system theme matching)
|
|
||||||
if (themeExplicit) {
|
|
||||||
return {
|
|
||||||
theme: themeChoice,
|
|
||||||
useSystemTheme: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise assume the defaults for the settings
|
|
||||||
return {
|
|
||||||
theme: themeChoice,
|
|
||||||
useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_onAction = (payload) => {
|
_onAction = (payload) => {
|
||||||
if (payload.action === 'id_server_changed') {
|
if (payload.action === 'id_server_changed') {
|
||||||
this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())});
|
this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())});
|
||||||
|
@ -219,33 +181,6 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
PlatformPeg.get().reload();
|
PlatformPeg.get().reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onThemeChange = (e) => {
|
|
||||||
const newTheme = e.target.value;
|
|
||||||
if (this.state.theme === newTheme) return;
|
|
||||||
|
|
||||||
// doing getValue in the .catch will still return the value we failed to set,
|
|
||||||
// so remember what the value was before we tried to set it so we can revert
|
|
||||||
const oldTheme = SettingsStore.getValue('theme');
|
|
||||||
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => {
|
|
||||||
dis.dispatch({action: 'recheck_theme'});
|
|
||||||
this.setState({theme: oldTheme});
|
|
||||||
});
|
|
||||||
this.setState({theme: newTheme});
|
|
||||||
// The settings watcher doesn't fire until the echo comes back from the
|
|
||||||
// server, so to make the theme change immediately we need to manually
|
|
||||||
// do the dispatch now
|
|
||||||
// XXX: The local echoed value appears to be unreliable, in particular
|
|
||||||
// when settings custom themes(!) so adding forceTheme to override
|
|
||||||
// the value from settings.
|
|
||||||
dis.dispatch({action: 'recheck_theme', forceTheme: newTheme});
|
|
||||||
};
|
|
||||||
|
|
||||||
_onUseSystemThemeChanged = (checked) => {
|
|
||||||
this.setState({useSystemTheme: checked});
|
|
||||||
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked);
|
|
||||||
dis.dispatch({action: 'recheck_theme'});
|
|
||||||
};
|
|
||||||
|
|
||||||
_onPasswordChangeError = (err) => {
|
_onPasswordChangeError = (err) => {
|
||||||
// TODO: Figure out a design that doesn't involve replacing the current dialog
|
// TODO: Figure out a design that doesn't involve replacing the current dialog
|
||||||
let errMsg = err.error || "";
|
let errMsg = err.error || "";
|
||||||
|
@ -282,41 +217,6 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onAddCustomTheme = async () => {
|
|
||||||
let currentThemes = SettingsStore.getValue("custom_themes");
|
|
||||||
if (!currentThemes) currentThemes = [];
|
|
||||||
currentThemes = currentThemes.map(c => c); // cheap clone
|
|
||||||
|
|
||||||
if (this._themeTimer) {
|
|
||||||
clearTimeout(this._themeTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const r = await fetch(this.state.customThemeUrl);
|
|
||||||
const themeInfo = await r.json();
|
|
||||||
if (!themeInfo || typeof(themeInfo['name']) !== 'string' || typeof(themeInfo['colors']) !== 'object') {
|
|
||||||
this.setState({customThemeMessage: {text: _t("Invalid theme schema."), isError: true}});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
currentThemes.push(themeInfo);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
this.setState({customThemeMessage: {text: _t("Error downloading theme information."), isError: true}});
|
|
||||||
return; // Don't continue on error
|
|
||||||
}
|
|
||||||
|
|
||||||
await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes);
|
|
||||||
this.setState({customThemeUrl: "", customThemeMessage: {text: _t("Theme added!"), isError: false}});
|
|
||||||
|
|
||||||
this._themeTimer = setTimeout(() => {
|
|
||||||
this.setState({customThemeMessage: {text: "", isError: false}});
|
|
||||||
}, 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
_onCustomThemeChange = (e) => {
|
|
||||||
this.setState({customThemeUrl: e.target.value});
|
|
||||||
};
|
|
||||||
|
|
||||||
_renderProfileSection() {
|
_renderProfileSection() {
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab_section">
|
<div className="mx_SettingsTab_section">
|
||||||
|
@ -401,77 +301,6 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderThemeSection() {
|
|
||||||
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
|
|
||||||
const LabelledToggleSwitch = sdk.getComponent("views.elements.LabelledToggleSwitch");
|
|
||||||
|
|
||||||
const themeWatcher = new ThemeWatcher();
|
|
||||||
let systemThemeSection;
|
|
||||||
if (themeWatcher.isSystemThemeSupported()) {
|
|
||||||
systemThemeSection = <div>
|
|
||||||
<LabelledToggleSwitch
|
|
||||||
value={this.state.useSystemTheme}
|
|
||||||
label={SettingsStore.getDisplayName("use_system_theme")}
|
|
||||||
onChange={this._onUseSystemThemeChanged}
|
|
||||||
/>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let customThemeForm;
|
|
||||||
if (SettingsStore.isFeatureEnabled("feature_custom_themes")) {
|
|
||||||
let messageElement = null;
|
|
||||||
if (this.state.customThemeMessage.text) {
|
|
||||||
if (this.state.customThemeMessage.isError) {
|
|
||||||
messageElement = <div className='text-error'>{this.state.customThemeMessage.text}</div>;
|
|
||||||
} else {
|
|
||||||
messageElement = <div className='text-success'>{this.state.customThemeMessage.text}</div>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customThemeForm = (
|
|
||||||
<div className='mx_SettingsTab_section'>
|
|
||||||
<form onSubmit={this._onAddCustomTheme}>
|
|
||||||
<Field
|
|
||||||
label={_t("Custom theme URL")}
|
|
||||||
type='text'
|
|
||||||
autoComplete="off"
|
|
||||||
onChange={this._onCustomThemeChange}
|
|
||||||
value={this.state.customThemeUrl}
|
|
||||||
/>
|
|
||||||
<AccessibleButton
|
|
||||||
onClick={this._onAddCustomTheme}
|
|
||||||
type="submit" kind="primary_sm"
|
|
||||||
disabled={!this.state.customThemeUrl.trim()}
|
|
||||||
>{_t("Add theme")}</AccessibleButton>
|
|
||||||
{messageElement}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const themes = Object.entries(enumerateThemes())
|
|
||||||
.map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability
|
|
||||||
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
|
|
||||||
const customThemes = themes.filter(p => !builtInThemes.includes(p))
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
const orderedThemes = [...builtInThemes, ...customThemes];
|
|
||||||
return (
|
|
||||||
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_themeSection">
|
|
||||||
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
|
|
||||||
{systemThemeSection}
|
|
||||||
<Field label={_t("Theme")} element="select"
|
|
||||||
value={this.state.theme} onChange={this._onThemeChange}
|
|
||||||
disabled={this.state.useSystemTheme}
|
|
||||||
>
|
|
||||||
{orderedThemes.map(theme => {
|
|
||||||
return <option key={theme.id} value={theme.id}>{theme.name}</option>;
|
|
||||||
})}
|
|
||||||
</Field>
|
|
||||||
{customThemeForm}
|
|
||||||
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_renderDiscoverySection() {
|
_renderDiscoverySection() {
|
||||||
const SetIdServer = sdk.getComponent("views.settings.SetIdServer");
|
const SetIdServer = sdk.getComponent("views.settings.SetIdServer");
|
||||||
|
|
||||||
|
@ -560,7 +389,6 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
{this._renderProfileSection()}
|
{this._renderProfileSection()}
|
||||||
{this._renderAccountSection()}
|
{this._renderAccountSection()}
|
||||||
{this._renderLanguageSection()}
|
{this._renderLanguageSection()}
|
||||||
{this._renderThemeSection()}
|
|
||||||
<div className="mx_SettingsTab_heading">{discoWarning} {_t("Discovery")}</div>
|
<div className="mx_SettingsTab_heading">{discoWarning} {_t("Discovery")}</div>
|
||||||
{this._renderDiscoverySection()}
|
{this._renderDiscoverySection()}
|
||||||
{this._renderIntegrationManagerSection() /* Has its own title */}
|
{this._renderIntegrationManagerSection() /* Has its own title */}
|
||||||
|
|
|
@ -400,6 +400,7 @@
|
||||||
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
|
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
|
||||||
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
|
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
|
||||||
"Failed to join room": "Failed to join room",
|
"Failed to join room": "Failed to join room",
|
||||||
|
"Font scaling": "Font scaling",
|
||||||
"Message Pinning": "Message Pinning",
|
"Message Pinning": "Message Pinning",
|
||||||
"Custom user status messages": "Custom user status messages",
|
"Custom user status messages": "Custom user status messages",
|
||||||
"Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)",
|
"Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)",
|
||||||
|
@ -410,6 +411,8 @@
|
||||||
"Use IRC layout": "Use IRC layout",
|
"Use IRC layout": "Use IRC layout",
|
||||||
"Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session",
|
"Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session",
|
||||||
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
||||||
|
"Font size": "Font size",
|
||||||
|
"Custom font size": "Custom font size",
|
||||||
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
|
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
|
||||||
"Use compact timeline layout": "Use compact timeline layout",
|
"Use compact timeline layout": "Use compact timeline layout",
|
||||||
"Show a placeholder for removed messages": "Show a placeholder for removed messages",
|
"Show a placeholder for removed messages": "Show a placeholder for removed messages",
|
||||||
|
@ -748,22 +751,26 @@
|
||||||
"Use an Integration Manager to manage bots, widgets, and sticker packs.": "Use an Integration Manager to manage bots, widgets, and sticker packs.",
|
"Use an Integration Manager to manage bots, widgets, and sticker packs.": "Use an Integration Manager to manage bots, widgets, and sticker packs.",
|
||||||
"Manage integrations": "Manage integrations",
|
"Manage integrations": "Manage integrations",
|
||||||
"Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.",
|
"Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.",
|
||||||
|
"Size must be a number": "Size must be a number",
|
||||||
|
"Custom font size can only be between %(min)s pt and %(max)s pt": "Custom font size can only be between %(min)s pt and %(max)s pt",
|
||||||
|
"Use between %(min)s pt and %(max)s pt": "Use between %(min)s pt and %(max)s pt",
|
||||||
|
"Invalid theme schema.": "Invalid theme schema.",
|
||||||
|
"Error downloading theme information.": "Error downloading theme information.",
|
||||||
|
"Theme added!": "Theme added!",
|
||||||
|
"Appearance": "Appearance",
|
||||||
|
"Custom theme URL": "Custom theme URL",
|
||||||
|
"Add theme": "Add theme",
|
||||||
|
"Theme": "Theme",
|
||||||
"Flair": "Flair",
|
"Flair": "Flair",
|
||||||
"Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
|
"Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
|
||||||
"Success": "Success",
|
"Success": "Success",
|
||||||
"Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them",
|
"Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them",
|
||||||
"Invalid theme schema.": "Invalid theme schema.",
|
|
||||||
"Error downloading theme information.": "Error downloading theme information.",
|
|
||||||
"Theme added!": "Theme added!",
|
|
||||||
"Profile": "Profile",
|
"Profile": "Profile",
|
||||||
"Email addresses": "Email addresses",
|
"Email addresses": "Email addresses",
|
||||||
"Phone numbers": "Phone numbers",
|
"Phone numbers": "Phone numbers",
|
||||||
"Set a new account password...": "Set a new account password...",
|
"Set a new account password...": "Set a new account password...",
|
||||||
"Account": "Account",
|
"Account": "Account",
|
||||||
"Language and region": "Language and region",
|
"Language and region": "Language and region",
|
||||||
"Custom theme URL": "Custom theme URL",
|
|
||||||
"Add theme": "Add theme",
|
|
||||||
"Theme": "Theme",
|
|
||||||
"Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.",
|
"Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.",
|
||||||
"Account management": "Account management",
|
"Account management": "Account management",
|
||||||
"Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!",
|
"Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!",
|
||||||
|
|
|
@ -29,6 +29,7 @@ import ThemeController from './controllers/ThemeController';
|
||||||
import PushToMatrixClientController from './controllers/PushToMatrixClientController';
|
import PushToMatrixClientController from './controllers/PushToMatrixClientController';
|
||||||
import ReloadOnChangeController from "./controllers/ReloadOnChangeController";
|
import ReloadOnChangeController from "./controllers/ReloadOnChangeController";
|
||||||
import {RIGHT_PANEL_PHASES} from "../stores/RightPanelStorePhases";
|
import {RIGHT_PANEL_PHASES} from "../stores/RightPanelStorePhases";
|
||||||
|
import FontSizeController from './controllers/FontSizeController';
|
||||||
|
|
||||||
// These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
|
// 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 = ['device', 'room-device', 'room-account', 'account', 'config'];
|
||||||
|
@ -94,6 +95,12 @@ export const SETTINGS = {
|
||||||
// // not use this for new settings.
|
// // not use this for new settings.
|
||||||
// invertedSettingName: "my-negative-setting",
|
// invertedSettingName: "my-negative-setting",
|
||||||
// },
|
// },
|
||||||
|
"feature_font_scaling": {
|
||||||
|
isFeature: true,
|
||||||
|
displayName: _td("Font scaling"),
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"feature_pinning": {
|
"feature_pinning": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
displayName: _td("Message Pinning"),
|
displayName: _td("Message Pinning"),
|
||||||
|
@ -164,6 +171,17 @@ export const SETTINGS = {
|
||||||
displayName: _td("Show info about bridges in room settings"),
|
displayName: _td("Show info about bridges in room settings"),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"fontSize": {
|
||||||
|
displayName: _td("Font size"),
|
||||||
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
|
default: 16,
|
||||||
|
controller: new FontSizeController(),
|
||||||
|
},
|
||||||
|
"useCustomFontSize": {
|
||||||
|
displayName: _td("Custom font size"),
|
||||||
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"MessageComposerInput.suggestEmoji": {
|
"MessageComposerInput.suggestEmoji": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
displayName: _td('Enable Emoji suggestions while typing'),
|
displayName: _td('Enable Emoji suggestions while typing'),
|
||||||
|
|
|
@ -370,6 +370,21 @@ export default class SettingsStore {
|
||||||
return SettingsStore._getFinalValue(setting, level, roomId, null, null);
|
return SettingsStore._getFinalValue(setting, level, roomId, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the default value of a setting.
|
||||||
|
* @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.
|
||||||
|
* @return {*} The default value
|
||||||
|
*/
|
||||||
|
static getDefaultValue(settingName) {
|
||||||
|
// Verify that the setting is actually a setting
|
||||||
|
if (!SETTINGS[settingName]) {
|
||||||
|
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return SETTINGS[settingName].default;
|
||||||
|
}
|
||||||
|
|
||||||
static _getFinalValue(setting, level, roomId, calculatedValue, calculatedAtLevel) {
|
static _getFinalValue(setting, level, roomId, calculatedValue, calculatedAtLevel) {
|
||||||
let resultingValue = calculatedValue;
|
let resultingValue = calculatedValue;
|
||||||
|
|
||||||
|
|
32
src/settings/controllers/FontSizeController.js
Normal file
32
src/settings/controllers/FontSizeController.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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";
|
||||||
|
import dis from "../../dispatcher/dispatcher";
|
||||||
|
|
||||||
|
export default class FontSizeController extends SettingController {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(level, roomId, newValue) {
|
||||||
|
// Dispatch font size change so that everything open responds to the change.
|
||||||
|
dis.dispatch({
|
||||||
|
action: "update-font-size",
|
||||||
|
size: newValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -81,7 +81,7 @@ export class ThemeWatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
getEffectiveTheme() {
|
getEffectiveTheme() {
|
||||||
// Dev note: Much of this logic is replicated in the GeneralUserSettingsTab
|
// Dev note: Much of this logic is replicated in the AppearanceUserSettingsTab
|
||||||
|
|
||||||
// XXX: checking the isLight flag here makes checking it in the ThemeController
|
// XXX: checking the isLight flag here makes checking it in the ThemeController
|
||||||
// itself completely redundant since we just override the result here and we're
|
// itself completely redundant since we just override the result here and we're
|
||||||
|
|
|
@ -14,7 +14,14 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* Simple utils for formatting style values
|
||||||
|
*/
|
||||||
|
|
||||||
// converts a pixel value to rem.
|
// converts a pixel value to rem.
|
||||||
export default function(pixelVal) {
|
export function toRem(pixelValue: number): string {
|
||||||
return pixelVal / 15 + "rem";
|
return pixelValue / 15 + "rem";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toPx(pixelValue: number): string {
|
||||||
|
return pixelValue + "px";
|
||||||
}
|
}
|
|
@ -206,7 +206,7 @@ describe("<TextualBody />", () => {
|
||||||
'Hey <span>' +
|
'Hey <span>' +
|
||||||
'<a class="mx_Pill mx_UserPill" title="@user:server">' +
|
'<a class="mx_Pill mx_UserPill" title="@user:server">' +
|
||||||
'<img class="mx_BaseAvatar mx_BaseAvatar_image" src="mxc://avatar.url/image.png" ' +
|
'<img class="mx_BaseAvatar mx_BaseAvatar_image" src="mxc://avatar.url/image.png" ' +
|
||||||
'style="width: 1.0666666666666667rem; height: 1.0666666666666667rem;" ' +
|
'style="width: 16px; height: 16px;" ' +
|
||||||
'title="@member:domain.bla" alt="" aria-hidden="true">Member</a>' +
|
'title="@member:domain.bla" alt="" aria-hidden="true">Member</a>' +
|
||||||
'</span></span>');
|
'</span></span>');
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue