{_t("Theme")}
{systemThemeSection}
-
- {orderedThemes.map(theme => {
- return
- {theme.name}
- ;
- })}
+
+ ({
+ value: t.id,
+ label: t.name,
+ disabled: this.state.useSystemTheme,
+ className: "mx_ThemeSelector_" + t.id,
+ }))}
+ onChange={this.onThemeChange}
+ value={this.state.useSystemTheme ? undefined : this.state.theme}
+ />
{customThemeForm}
@@ -391,7 +390,13 @@ export default class AppearanceUserSettingsTab extends React.Component
+ advanced = <>
+
- ;
+ >;
}
return
{toggle}
diff --git a/src/hooks/useAccountData.ts b/src/hooks/useAccountData.ts
new file mode 100644
index 0000000000..dd0d53f0d3
--- /dev/null
+++ b/src/hooks/useAccountData.ts
@@ -0,0 +1,50 @@
+/*
+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 {useCallback, useState} from "react";
+import {MatrixClient} from "matrix-js-sdk/src/client";
+import {MatrixEvent} from "matrix-js-sdk/src/models/event";
+import {Room} from "matrix-js-sdk/src/models/room";
+
+import {useEventEmitter} from "./useEventEmitter";
+
+const tryGetContent = (ev?: MatrixEvent) => ev ? ev.getContent() : undefined;
+
+// Hook to simplify listening to Matrix account data
+export const useAccountData = (cli: MatrixClient, eventType: string) => {
+ const [value, setValue] = useState(() => tryGetContent(cli.getAccountData(eventType)));
+
+ const handler = useCallback((event) => {
+ if (event.getType() !== eventType) return;
+ setValue(event.getContent());
+ }, [cli, eventType]);
+ useEventEmitter(cli, "accountData", handler);
+
+ return value || {} as T;
+};
+
+// Hook to simplify listening to Matrix room account data
+export const useRoomAccountData = (room: Room, eventType: string) => {
+ const [value, setValue] = useState(() => tryGetContent(room.getAccountData(eventType)));
+
+ const handler = useCallback((event) => {
+ if (event.getType() !== eventType) return;
+ setValue(event.getContent());
+ }, [room, eventType]);
+ useEventEmitter(room, "Room.accountData", handler);
+
+ return value || {} as T;
+};
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 86d5f488ff..d721979329 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -479,6 +479,7 @@
"%(senderName)s invited %(targetName)s": "%(senderName)s invited %(targetName)s",
"You changed the room topic": "You changed the room topic",
"%(senderName)s changed the room topic": "%(senderName)s changed the room topic",
+ "New spinner design": "New spinner design",
"Font scaling": "Font scaling",
"Message Pinning": "Message Pinning",
"Custom user status messages": "Custom user status messages",
@@ -493,7 +494,7 @@
"Font size": "Font size",
"Use custom size": "Use custom size",
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
- "Use compact timeline layout": "Use compact timeline layout",
+ "Use a more compact ‘Modern’ layout": "Use a more compact ‘Modern’ layout",
"Show a placeholder for removed messages": "Show a placeholder for removed messages",
"Show join/leave messages (invites/kicks/bans unaffected)": "Show join/leave messages (invites/kicks/bans unaffected)",
"Show avatar changes": "Show avatar changes",
diff --git a/src/notifications/ContentRules.js b/src/notifications/ContentRules.ts
similarity index 69%
rename from src/notifications/ContentRules.js
rename to src/notifications/ContentRules.ts
index 8c285220c7..a3ec017e37 100644
--- a/src/notifications/ContentRules.js
+++ b/src/notifications/ContentRules.ts
@@ -1,6 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+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.
@@ -15,9 +15,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-'use strict';
+import {PushRuleVectorState, State} from "./PushRuleVectorState";
+import {IExtendedPushRule, IPushRuleSet, IRuleSets} from "./types";
-import {PushRuleVectorState} from "./PushRuleVectorState";
+export interface IContentRules {
+ vectorState: State;
+ rules: IExtendedPushRule[];
+ externalRules: IExtendedPushRule[];
+}
+
+export const SCOPE = "global";
+export const KIND = "content";
export class ContentRules {
/**
@@ -31,7 +39,7 @@ export class ContentRules {
* externalRules: a list of other keyword rules, with states other than
* vectorState
*/
- static parseContentRules(rulesets) {
+ static parseContentRules(rulesets: IRuleSets): IContentRules {
// first categorise the keyword rules in terms of their actions
const contentRules = this._categoriseContentRules(rulesets);
@@ -51,59 +59,72 @@ export class ContentRules {
if (contentRules.loud.length) {
return {
- vectorState: PushRuleVectorState.LOUD,
+ vectorState: State.Loud,
rules: contentRules.loud,
- externalRules: [].concat(contentRules.loud_but_disabled, contentRules.on, contentRules.on_but_disabled, contentRules.other),
+ externalRules: [
+ ...contentRules.loud_but_disabled,
+ ...contentRules.on,
+ ...contentRules.on_but_disabled,
+ ...contentRules.other,
+ ],
};
} else if (contentRules.loud_but_disabled.length) {
return {
- vectorState: PushRuleVectorState.OFF,
+ vectorState: State.Off,
rules: contentRules.loud_but_disabled,
- externalRules: [].concat(contentRules.on, contentRules.on_but_disabled, contentRules.other),
+ externalRules: [...contentRules.on, ...contentRules.on_but_disabled, ...contentRules.other],
};
} else if (contentRules.on.length) {
return {
- vectorState: PushRuleVectorState.ON,
+ vectorState: State.On,
rules: contentRules.on,
- externalRules: [].concat(contentRules.on_but_disabled, contentRules.other),
+ externalRules: [...contentRules.on_but_disabled, ...contentRules.other],
};
} else if (contentRules.on_but_disabled.length) {
return {
- vectorState: PushRuleVectorState.OFF,
+ vectorState: State.Off,
rules: contentRules.on_but_disabled,
externalRules: contentRules.other,
};
} else {
return {
- vectorState: PushRuleVectorState.ON,
+ vectorState: State.On,
rules: [],
externalRules: contentRules.other,
};
}
}
- static _categoriseContentRules(rulesets) {
- const contentRules = {on: [], on_but_disabled: [], loud: [], loud_but_disabled: [], other: []};
+ static _categoriseContentRules(rulesets: IRuleSets) {
+ const contentRules: Record<"on"|"on_but_disabled"|"loud"|"loud_but_disabled"|"other", IExtendedPushRule[]> = {
+ on: [],
+ on_but_disabled: [],
+ loud: [],
+ loud_but_disabled: [],
+ other: [],
+ };
+
for (const kind in rulesets.global) {
for (let i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) {
const r = rulesets.global[kind][i];
// check it's not a default rule
- if (r.rule_id[0] === '.' || kind !== 'content') {
+ if (r.rule_id[0] === '.' || kind !== "content") {
continue;
}
- r.kind = kind; // is this needed? not sure
+ // this is needed as we are flattening an object of arrays into a single array
+ r.kind = kind;
switch (PushRuleVectorState.contentRuleVectorStateKind(r)) {
- case PushRuleVectorState.ON:
+ case State.On:
if (r.enabled) {
contentRules.on.push(r);
} else {
contentRules.on_but_disabled.push(r);
}
break;
- case PushRuleVectorState.LOUD:
+ case State.Loud:
if (r.enabled) {
contentRules.loud.push(r);
} else {
diff --git a/src/notifications/NotificationUtils.js b/src/notifications/NotificationUtils.ts
similarity index 80%
rename from src/notifications/NotificationUtils.js
rename to src/notifications/NotificationUtils.ts
index bf393da060..e3b7f66447 100644
--- a/src/notifications/NotificationUtils.js
+++ b/src/notifications/NotificationUtils.ts
@@ -1,6 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+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.
@@ -15,7 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-'use strict';
+import {Action, Actions} from "./types";
+
+interface IEncodedActions {
+ notify: boolean;
+ sound?: string;
+ highlight?: boolean;
+}
export class NotificationUtils {
// Encodes a dictionary of {
@@ -24,12 +30,12 @@ export class NotificationUtils {
// "highlight: true/false,
// }
// to a list of push actions.
- static encodeActions(action) {
+ static encodeActions(action: IEncodedActions) {
const notify = action.notify;
const sound = action.sound;
const highlight = action.highlight;
if (notify) {
- const actions = ["notify"];
+ const actions: Action[] = [Actions.Notify];
if (sound) {
actions.push({"set_tweak": "sound", "value": sound});
}
@@ -40,7 +46,7 @@ export class NotificationUtils {
}
return actions;
} else {
- return ["dont_notify"];
+ return [Actions.DontNotify];
}
}
@@ -50,18 +56,18 @@ export class NotificationUtils {
// "highlight: true/false,
// }
// If the actions couldn't be decoded then returns null.
- static decodeActions(actions) {
+ static decodeActions(actions: Action[]): IEncodedActions {
let notify = false;
let sound = null;
let highlight = false;
for (let i = 0; i < actions.length; ++i) {
const action = actions[i];
- if (action === "notify") {
+ if (action === Actions.Notify) {
notify = true;
- } else if (action === "dont_notify") {
+ } else if (action === Actions.DontNotify) {
notify = false;
- } else if (typeof action === 'object') {
+ } else if (typeof action === "object") {
if (action.set_tweak === "sound") {
sound = action.value;
} else if (action.set_tweak === "highlight") {
@@ -81,7 +87,7 @@ export class NotificationUtils {
highlight = true;
}
- const result = {notify: notify, highlight: highlight};
+ const result: IEncodedActions = { notify, highlight };
if (sound !== null) {
result.sound = sound;
}
diff --git a/src/notifications/PushRuleVectorState.js b/src/notifications/PushRuleVectorState.ts
similarity index 69%
rename from src/notifications/PushRuleVectorState.js
rename to src/notifications/PushRuleVectorState.ts
index 263226ce1c..d33426cfc4 100644
--- a/src/notifications/PushRuleVectorState.js
+++ b/src/notifications/PushRuleVectorState.ts
@@ -1,6 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+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.
@@ -15,43 +15,42 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-'use strict';
-
import {StandardActions} from "./StandardActions";
import {NotificationUtils} from "./NotificationUtils";
+import {IPushRule} from "./types";
+
+export enum State {
+ /** The push rule is disabled */
+ Off = "off",
+ /** The user will receive push notification for this rule */
+ On = "on",
+ /** The user will receive push notification for this rule with sound and
+ highlight if this is legitimate */
+ Loud = "loud",
+}
export class PushRuleVectorState {
- // Backwards compatibility (things should probably be using .states instead)
- static OFF = "off";
- static ON = "on";
- static LOUD = "loud";
+ // Backwards compatibility (things should probably be using the enum above instead)
+ static OFF = State.Off;
+ static ON = State.On;
+ static LOUD = State.Loud;
/**
* Enum for state of a push rule as defined by the Vector UI.
* @readonly
* @enum {string}
*/
- static states = {
- /** The push rule is disabled */
- OFF: PushRuleVectorState.OFF,
-
- /** The user will receive push notification for this rule */
- ON: PushRuleVectorState.ON,
-
- /** The user will receive push notification for this rule with sound and
- highlight if this is legitimate */
- LOUD: PushRuleVectorState.LOUD,
- };
+ static states = State;
/**
* Convert a PushRuleVectorState to a list of actions
*
* @return [object] list of push-rule actions
*/
- static actionsFor(pushRuleVectorState) {
- if (pushRuleVectorState === PushRuleVectorState.ON) {
+ static actionsFor(pushRuleVectorState: State) {
+ if (pushRuleVectorState === State.On) {
return StandardActions.ACTION_NOTIFY;
- } else if (pushRuleVectorState === PushRuleVectorState.LOUD) {
+ } else if (pushRuleVectorState === State.Loud) {
return StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND;
}
}
@@ -63,7 +62,7 @@ export class PushRuleVectorState {
* category or in PushRuleVectorState.LOUD, regardless of its enabled
* state. Returns null if it does not match these categories.
*/
- static contentRuleVectorStateKind(rule) {
+ static contentRuleVectorStateKind(rule: IPushRule): State {
const decoded = NotificationUtils.decodeActions(rule.actions);
if (!decoded) {
@@ -81,10 +80,10 @@ export class PushRuleVectorState {
let stateKind = null;
switch (tweaks) {
case 0:
- stateKind = PushRuleVectorState.ON;
+ stateKind = State.On;
break;
case 2:
- stateKind = PushRuleVectorState.LOUD;
+ stateKind = State.Loud;
break;
}
return stateKind;
diff --git a/src/notifications/StandardActions.js b/src/notifications/StandardActions.ts
similarity index 98%
rename from src/notifications/StandardActions.js
rename to src/notifications/StandardActions.ts
index b54cea332a..c17010af9a 100644
--- a/src/notifications/StandardActions.js
+++ b/src/notifications/StandardActions.ts
@@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-'use strict';
-
import {NotificationUtils} from "./NotificationUtils";
const encodeActions = NotificationUtils.encodeActions;
diff --git a/src/notifications/types.ts b/src/notifications/types.ts
new file mode 100644
index 0000000000..9622193740
--- /dev/null
+++ b/src/notifications/types.ts
@@ -0,0 +1,111 @@
+/*
+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.
+*/
+
+export enum NotificationSetting {
+ AllMessages = "all_messages", // .m.rule.message = notify
+ DirectMessagesMentionsKeywords = "dm_mentions_keywords", // .m.rule.message = mark_unread. This is the new default.
+ MentionsKeywordsOnly = "mentions_keywords", // .m.rule.message = mark_unread; .m.rule.room_one_to_one = mark_unread
+ Never = "never", // .m.rule.master = enabled (dont_notify)
+}
+
+export interface ISoundTweak {
+ set_tweak: "sound";
+ value: string;
+}
+export interface IHighlightTweak {
+ set_tweak: "highlight";
+ value?: boolean;
+}
+
+export type Tweak = ISoundTweak | IHighlightTweak;
+
+export enum Actions {
+ Notify = "notify",
+ DontNotify = "dont_notify", // no-op
+ Coalesce = "coalesce", // unused
+ MarkUnread = "mark_unread", // new
+}
+
+export type Action = Actions | Tweak;
+
+// Push rule kinds in descending priority order
+export enum Kind {
+ Override = "override",
+ ContentSpecific = "content",
+ RoomSpecific = "room",
+ SenderSpecific = "sender",
+ Underride = "underride",
+}
+
+export interface IEventMatchCondition {
+ kind: "event_match";
+ key: string;
+ pattern: string;
+}
+
+export interface IContainsDisplayNameCondition {
+ kind: "contains_display_name";
+}
+
+export interface IRoomMemberCountCondition {
+ kind: "room_member_count";
+ is: string;
+}
+
+export interface ISenderNotificationPermissionCondition {
+ kind: "sender_notification_permission";
+ key: string;
+}
+
+export type Condition =
+ IEventMatchCondition |
+ IContainsDisplayNameCondition |
+ IRoomMemberCountCondition |
+ ISenderNotificationPermissionCondition;
+
+export enum RuleIds {
+ MasterRule = ".m.rule.master", // The master rule (all notifications disabling)
+ MessageRule = ".m.rule.message",
+ EncryptedMessageRule = ".m.rule.encrypted",
+ RoomOneToOneRule = ".m.rule.room_one_to_one",
+ EncryptedRoomOneToOneRule = ".m.rule.room_one_to_one",
+}
+
+export interface IPushRule {
+ enabled: boolean;
+ rule_id: RuleIds | string;
+ actions: Action[];
+ default: boolean;
+ conditions?: Condition[]; // only applicable to `underride` and `override` rules
+ pattern?: string; // only applicable to `content` rules
+}
+
+// push rule extended with kind, used by ContentRules and js-sdk's pushprocessor
+export interface IExtendedPushRule extends IPushRule {
+ kind: Kind;
+}
+
+export interface IPushRuleSet {
+ override: IPushRule[];
+ content: IPushRule[];
+ room: IPushRule[];
+ sender: IPushRule[];
+ underride: IPushRule[];
+}
+
+export interface IRuleSets {
+ global: IPushRuleSet;
+}
diff --git a/src/settings/Settings.js b/src/settings/Settings.js
index eb882b2d18..820329f6c6 100644
--- a/src/settings/Settings.js
+++ b/src/settings/Settings.js
@@ -97,6 +97,12 @@ export const SETTINGS = {
// // not use this for new settings.
// invertedSettingName: "my-negative-setting",
// },
+ "feature_new_spinner": {
+ isFeature: true,
+ displayName: _td("New spinner design"),
+ supportedLevels: LEVELS_FEATURE,
+ default: false,
+ },
"feature_font_scaling": {
isFeature: true,
displayName: _td("Font scaling"),
@@ -192,12 +198,12 @@ export const SETTINGS = {
},
// TODO: Wire up appropriately to UI (FTUE notifications)
"Notifications.alwaysShowBadgeCounts": {
- supportedLevels: ['account'],
+ supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
default: false,
},
"useCompactLayout": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
- displayName: _td('Use compact timeline layout'),
+ displayName: _td('Use a more compact ‘Modern’ layout'),
default: false,
},
"showRedactions": {
diff --git a/src/stores/room-list/ListLayout.ts b/src/stores/room-list/ListLayout.ts
index ebc7b95854..8ca8ad637b 100644
--- a/src/stores/room-list/ListLayout.ts
+++ b/src/stores/room-list/ListLayout.ts
@@ -18,6 +18,10 @@ import { TagID } from "./models";
const TILE_HEIGHT_PX = 44;
+// the .65 comes from the CSS where the show more button is
+// mathematically 65% of a tile when floating.
+const RESIZER_BOX_FACTOR = 0.65;
+
interface ISerializedListLayout {
numTiles: number;
showPreviews: boolean;
@@ -67,6 +71,7 @@ export class ListLayout {
}
public get visibleTiles(): number {
+ if (this._n === 0) return this.defaultVisibleTiles;
return Math.max(this._n, this.minVisibleTiles);
}
@@ -76,9 +81,13 @@ export class ListLayout {
}
public get minVisibleTiles(): number {
- // the .65 comes from the CSS where the show more button is
- // mathematically 65% of a tile when floating.
- return 4.65;
+ return 1 + RESIZER_BOX_FACTOR;
+ }
+
+ public get defaultVisibleTiles(): number {
+ // TODO: Remove dogfood flag
+ const val = Number(localStorage.getItem("mx_dogfood_rl_defTiles") || 4);
+ return val + RESIZER_BOX_FACTOR;
}
public calculateTilesToPixelsMin(maxTiles: number, n: number, possiblePadding: number): number {
@@ -92,6 +101,10 @@ export class ListLayout {
return this.tilesToPixels(Math.min(maxTiles, n)) + padding;
}
+ public tilesWithResizerBoxFactor(n: number): number {
+ return n + RESIZER_BOX_FACTOR;
+ }
+
public tilesWithPadding(n: number, paddingPx: number): number {
return this.pixelsToTiles(this.tilesToPixelsWithPadding(n, paddingPx));
}
diff --git a/src/toasts/AnalyticsToast.tsx b/src/toasts/AnalyticsToast.tsx
index 7cd59222dd..b186a65d9d 100644
--- a/src/toasts/AnalyticsToast.tsx
+++ b/src/toasts/AnalyticsToast.tsx
@@ -24,14 +24,12 @@ import GenericToast from "../components/views/toasts/GenericToast";
import ToastStore from "../stores/ToastStore";
const onAccept = () => {
- console.log("DEBUG onAccept AnalyticsToast");
dis.dispatch({
action: 'accept_cookies',
});
};
const onReject = () => {
- console.log("DEBUG onReject AnalyticsToast");
dis.dispatch({
action: "reject_cookies",
});