From 23383419e803f6916c6636de10865b386a240f73 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 13:19:54 -0600 Subject: [PATCH 01/11] Add settings base for Mjolnir rules --- .../views/dialogs/_UserSettingsDialog.scss | 4 + .../views/dialogs/UserSettingsDialog.js | 30 ++++++++ .../tabs/user/MjolnirUserSettingsTab.js | 74 +++++++++++++++++++ src/i18n/strings/en_EN.json | 11 ++- src/settings/Settings.js | 14 ++++ 5 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js diff --git a/res/css/views/dialogs/_UserSettingsDialog.scss b/res/css/views/dialogs/_UserSettingsDialog.scss index 2a046ff501..4d831d7858 100644 --- a/res/css/views/dialogs/_UserSettingsDialog.scss +++ b/res/css/views/dialogs/_UserSettingsDialog.scss @@ -45,6 +45,10 @@ limitations under the License. mask-image: url('$(res)/img/feather-customised/flag.svg'); } +.mx_UserSettingsDialog_mjolnirIcon::before { + mask-image: url('$(res)/img/feather-customised/face.svg'); +} + .mx_UserSettingsDialog_flairIcon::before { mask-image: url('$(res)/img/feather-customised/flair.svg'); } diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index fb9045f05a..6e324ad3fb 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 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. @@ -29,12 +30,34 @@ import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab"; import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab"; import sdk from "../../../index"; import SdkConfig from "../../../SdkConfig"; +import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab"; export default class UserSettingsDialog extends React.Component { static propTypes = { onFinished: PropTypes.func.isRequired, }; + constructor() { + super(); + + this.state = { + mjolnirEnabled: SettingsStore.isFeatureEnabled("feature_mjolnir"), + } + } + + componentDidMount(): void { + this._mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this._mjolnirChanged.bind(this)); + } + + componentWillUnmount(): void { + SettingsStore.unwatchSetting(this._mjolnirWatcher); + } + + _mjolnirChanged(settingName, roomId, atLevel, newValue) { + // We can cheat because we know what levels a feature is tracked at, and how it is tracked + this.setState({mjolnirEnabled: newValue}); + } + _getTabs() { const tabs = []; @@ -75,6 +98,13 @@ export default class UserSettingsDialog extends React.Component { , )); } + if (this.state.mjolnirEnabled) { + tabs.push(new Tab( + _td("Ignored users"), + "mx_UserSettingsDialog_mjolnirIcon", + , + )); + } tabs.push(new Tab( _td("Help & About"), "mx_UserSettingsDialog_helpIcon", diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js new file mode 100644 index 0000000000..02e64c0bc1 --- /dev/null +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js @@ -0,0 +1,74 @@ +/* +Copyright 2019 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"; +const sdk = require("../../../../.."); + +export default class MjolnirUserSettingsTab extends React.Component { + constructor() { + super(); + } + + render() { + return ( +
+
{_t("Ignored users")}
+
+
+ {_t("⚠ These settings are meant for advanced users.")}
+
+ {_t( + "Add users and servers you want to ignore here. Use asterisks " + + "to have Riot match any characters. For example, @bot:* " + + "would ignore all users that have the name 'bot' on any server.", + {}, {code: (s) => {s}}, + )}
+
+ {_t( + "Ignoring people is done through ban lists which contain rules for " + + "who to ban. Subscribing to a ban list means the users/servers blocked by " + + "that list will be hidden from you." + )} +
+
+
+ {_t("Personal ban list")} +
+ {_t( + "Your personal ban list holds all the users/servers you personally don't " + + "want to see messages from. After ignoring your first user/server, a new room " + + "will show up in your room list named 'My Ban List' - stay in this room to keep " + + "the ban list in effect.", + )} +
+

TODO

+
+
+ {_t("Subscribed lists")} +
+ {_t("Subscribing to a ban list will cause you to join it!")} +   + {_t( + "If this isn't what you want, please use a different tool to ignore users.", + )} +
+

TODO

+
+
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 524a8a1abf..e909f49159 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -335,6 +335,7 @@ "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Use the new, consistent UserInfo panel for Room Members and Group Members": "Use the new, consistent UserInfo panel for Room Members and Group Members", + "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", @@ -637,6 +638,15 @@ "Access Token:": "Access Token:", "click to reveal": "click to reveal", "Labs": "Labs", + "Ignored users": "Ignored users", + "⚠ These settings are meant for advanced users.": "⚠ These settings are meant for advanced users.", + "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.", + "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.", + "Personal ban list": "Personal ban list", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.", + "Subscribed lists": "Subscribed lists", + "Subscribing to a ban list will cause you to join it!": "Subscribing to a ban list will cause you to join it!", + "If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.", "Notifications": "Notifications", "Start automatically after system login": "Start automatically after system login", "Always show the window menu bar": "Always show the window menu bar", @@ -654,7 +664,6 @@ "Cryptography": "Cryptography", "Device ID:": "Device ID:", "Device key:": "Device key:", - "Ignored users": "Ignored users", "Bulk options": "Bulk options", "Accept all %(invitedRooms)s invites": "Accept all %(invitedRooms)s invites", "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 7470641359..1cfff0182e 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -126,6 +126,20 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_mjolnir": { + isFeature: true, + displayName: _td("Try out new ways to ignore people (experimental)"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, + "mjolnirRooms": { + supportedLevels: ['account'], + default: [], + }, + "mjolnirPersonalRoom": { + supportedLevels: ['account'], + default: null, + }, "useCiderComposer": { displayName: _td("Use the new, faster, composer for writing messages"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, From e6e12df82d1e801019f3ea993b35ae0b2b61f04c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 13:20:08 -0600 Subject: [PATCH 02/11] Add structural base for handling Mjolnir lists --- package.json | 1 + src/i18n/strings/en_EN.json | 2 + src/mjolnir/BanList.js | 98 +++++++++++++++++++++++++++++ src/mjolnir/ListRule.js | 63 +++++++++++++++++++ src/mjolnir/Mjolnir.js | 122 ++++++++++++++++++++++++++++++++++++ src/utils/MatrixGlob.js | 54 ++++++++++++++++ yarn.lock | 5 ++ 7 files changed, 345 insertions(+) create mode 100644 src/mjolnir/BanList.js create mode 100644 src/mjolnir/ListRule.js create mode 100644 src/mjolnir/Mjolnir.js create mode 100644 src/utils/MatrixGlob.js diff --git a/package.json b/package.json index e709662020..745f82d7bc 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566", "gfm.css": "^1.1.1", "glob": "^5.0.14", + "glob-to-regexp": "^0.4.1", "highlight.js": "^9.15.8", "is-ip": "^2.0.0", "isomorphic-fetch": "^2.2.1", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e909f49159..770f4723ef 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -389,6 +389,8 @@ "Call invitation": "Call invitation", "Messages sent by bot": "Messages sent by bot", "When rooms are upgraded": "When rooms are upgraded", + "My Ban List": "My Ban List", + "This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!", "Active call (%(roomName)s)": "Active call (%(roomName)s)", "unknown caller": "unknown caller", "Incoming voice call from %(name)s": "Incoming voice call from %(name)s", diff --git a/src/mjolnir/BanList.js b/src/mjolnir/BanList.js new file mode 100644 index 0000000000..6ebc0a7e36 --- /dev/null +++ b/src/mjolnir/BanList.js @@ -0,0 +1,98 @@ +/* +Copyright 2019 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. +*/ + +// Inspiration largely taken from Mjolnir itself + +import {ListRule, RECOMMENDATION_BAN, recommendationToStable} from "./ListRule"; +import MatrixClientPeg from "../MatrixClientPeg"; + +export const RULE_USER = "m.room.rule.user"; +export const RULE_ROOM = "m.room.rule.room"; +export const RULE_SERVER = "m.room.rule.server"; + +export const USER_RULE_TYPES = [RULE_USER, "org.matrix.mjolnir.rule.user"]; +export const ROOM_RULE_TYPES = [RULE_ROOM, "org.matrix.mjolnir.rule.room"]; +export const SERVER_RULE_TYPES = [RULE_SERVER, "org.matrix.mjolnir.rule.server"]; +export const ALL_RULE_TYPES = [...USER_RULE_TYPES, ...ROOM_RULE_TYPES, ...SERVER_RULE_TYPES]; + +export function ruleTypeToStable(rule: string, unstable = true): string { + if (USER_RULE_TYPES.includes(rule)) return unstable ? USER_RULE_TYPES[USER_RULE_TYPES.length - 1] : RULE_USER; + if (ROOM_RULE_TYPES.includes(rule)) return unstable ? ROOM_RULE_TYPES[ROOM_RULE_TYPES.length - 1] : RULE_ROOM; + if (SERVER_RULE_TYPES.includes(rule)) return unstable ? SERVER_RULE_TYPES[SERVER_RULE_TYPES.length - 1] : RULE_SERVER; + return null; +} + +export class BanList { + _rules: ListRule[] = []; + _roomId: string; + + constructor(roomId: string) { + this._roomId = roomId; + this.updateList(); + } + + get roomId(): string { + return this._roomId; + } + + get serverRules(): ListRule[] { + return this._rules.filter(r => r.kind === RULE_SERVER); + } + + get userRules(): ListRule[] { + return this._rules.filter(r => r.kind === RULE_USER); + } + + get roomRules(): ListRule[] { + return this._rules.filter(r => r.kind === RULE_ROOM); + } + + banEntity(kind: string, entity: string, reason: string): Promise { + return MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), { + entity: entity, + reason: reason, + recommendation: recommendationToStable(RECOMMENDATION_BAN, true), + }, "rule:" + entity); + } + + unbanEntity(kind: string, entity: string): Promise { + // Empty state event is effectively deleting it. + return MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), {}, "rule:" + entity); + } + + updateList() { + this._rules = []; + + const room = MatrixClientPeg.get().getRoom(this._roomId); + if (!room) return; + + for (const eventType of ALL_RULE_TYPES) { + const events = room.currentState.getStateEvents(eventType, undefined); + for (const ev of events) { + if (!ev['state_key']) continue; + + const kind = ruleTypeToStable(eventType, false); + + const entity = ev.getContent()['entity']; + const recommendation = ev.getContent()['recommendation']; + const reason = ev.getContent()['reason']; + if (!entity || !recommendation || !reason) continue; + + this._rules.push(new ListRule(entity, recommendation, reason, kind)); + } + } + } +} diff --git a/src/mjolnir/ListRule.js b/src/mjolnir/ListRule.js new file mode 100644 index 0000000000..d33248d24c --- /dev/null +++ b/src/mjolnir/ListRule.js @@ -0,0 +1,63 @@ +/* +Copyright 2019 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 {MatrixGlob} from "../utils/MatrixGlob"; + +// Inspiration largely taken from Mjolnir itself + +export const RECOMMENDATION_BAN = "m.ban"; +export const RECOMMENDATION_BAN_TYPES = [RECOMMENDATION_BAN, "org.matrix.mjolnir.ban"]; + +export function recommendationToStable(recommendation: string, unstable = true): string { + if (RECOMMENDATION_BAN_TYPES.includes(recommendation)) return unstable ? RECOMMENDATION_BAN_TYPES[RECOMMENDATION_BAN_TYPES.length - 1] : RECOMMENDATION_BAN; + return null; +} + +export class ListRule { + _glob: MatrixGlob; + _entity: string; + _action: string; + _reason: string; + _kind: string; + + constructor(entity: string, action: string, reason: string, kind: string) { + this._glob = new MatrixGlob(entity); + this._entity = entity; + this._action = recommendationToStable(action, false); + this._reason = reason; + this._kind = kind; + } + + get entity(): string { + return this._entity; + } + + get reason(): string { + return this._reason; + } + + get kind(): string { + return this._kind; + } + + get recommendation(): string { + return this._action; + } + + isMatch(entity: string): boolean { + return this._glob.test(entity); + } +} diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.js new file mode 100644 index 0000000000..a12534592d --- /dev/null +++ b/src/mjolnir/Mjolnir.js @@ -0,0 +1,122 @@ +/* +Copyright 2019 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 MatrixClientPeg from "../MatrixClientPeg"; +import {ALL_RULE_TYPES, BanList} from "./BanList"; +import SettingsStore, {SettingLevel} from "../settings/SettingsStore"; +import {_t} from "../languageHandler"; + +// TODO: Move this and related files to the js-sdk or something once finalized. + +export class Mjolnir { + static _instance: Mjolnir = null; + + _lists: BanList[] = []; + _roomIds: string[] = []; + _mjolnirWatchRef = null; + + constructor() { + } + + start() { + this._updateLists(SettingsStore.getValue("mjolnirRooms")); + this._mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this._onListsChanged.bind(this)); + + MatrixClientPeg.get().on("RoomState.events", this._onEvent.bind(this)); + } + + stop() { + SettingsStore.unwatchSetting(this._mjolnirWatchRef); + MatrixClientPeg.get().removeListener("RoomState.events", this._onEvent.bind(this)); + } + + async getOrCreatePersonalList(): Promise { + let personalRoomId = SettingsStore.getValue("mjolnirPersonalRoom"); + if (!personalRoomId) { + const resp = await MatrixClientPeg.get().createRoom({ + name: _t("My Ban List"), + topic: _t("This is your list of users/servers you have blocked - don't leave the room!"), + preset: "private_chat" + }); + personalRoomId = resp['room_id']; + SettingsStore.setValue("mjolnirPersonalRoom", null, SettingLevel.ACCOUNT, personalRoomId); + SettingsStore.setValue("mjolnirRooms", null, SettingLevel.ACCOUNT, [personalRoomId, ...this._roomIds]); + } + if (!personalRoomId) { + throw new Error("Error finding a room ID to use"); + } + + let list = this._lists.find(b => b.roomId === personalRoomId); + if (!list) list = new BanList(personalRoomId); + // we don't append the list to the tracked rooms because it should already be there. + // we're just trying to get the caller some utility access to the list + + return list; + } + + _onEvent(event) { + if (!this._roomIds.includes(event.getRoomId())) return; + if (!ALL_RULE_TYPES.includes(event.getType())) return; + + this._updateLists(this._roomIds); + } + + _onListsChanged(settingName, roomId, atLevel, newValue) { + // We know that ban lists are only recorded at one level so we don't need to re-eval them + this._updateLists(newValue); + } + + _updateLists(listRoomIds: string[]) { + this._lists = []; + this._roomIds = listRoomIds || []; + if (!listRoomIds) return; + + for (const roomId of listRoomIds) { + // Creating the list updates it + this._lists.push(new BanList(roomId)); + } + } + + isServerBanned(serverName: string): boolean { + for (const list of this._lists) { + for (const rule of list.serverRules) { + if (rule.isMatch(serverName)) { + return true; + } + } + } + return false; + } + + isUserBanned(userId: string): boolean { + for (const list of this._lists) { + for (const rule of list.userRules) { + if (rule.isMatch(userId)) { + return true; + } + } + } + return false; + } + + static sharedInstance(): Mjolnir { + if (!Mjolnir._instance) { + Mjolnir._instance = new Mjolnir(); + } + return Mjolnir._instance; + } +} + diff --git a/src/utils/MatrixGlob.js b/src/utils/MatrixGlob.js new file mode 100644 index 0000000000..cf55040625 --- /dev/null +++ b/src/utils/MatrixGlob.js @@ -0,0 +1,54 @@ +/* +Copyright 2019 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 globToRegexp from "glob-to-regexp"; + +// Taken with permission from matrix-bot-sdk: +// https://github.com/turt2live/matrix-js-bot-sdk/blob/eb148c2ecec7bf3ade801d73deb43df042d55aef/src/MatrixGlob.ts + +/** + * Represents a common Matrix glob. This is commonly used + * for server ACLs and similar functions. + */ +export class MatrixGlob { + _regex: RegExp; + + /** + * Creates a new Matrix Glob + * @param {string} glob The glob to convert. Eg: "*.example.org" + */ + constructor(glob: string) { + const globRegex = globToRegexp(glob, { + extended: false, + globstar: false, + }); + + // We need to convert `?` manually because globToRegexp's extended mode + // does more than we want it to. + const replaced = globRegex.toString().replace(/\\\?/g, "."); + this._regex = new RegExp(replaced.substring(1, replaced.length - 1)); + } + + /** + * Tests the glob against a value, returning true if it matches. + * @param {string} val The value to test. + * @returns {boolean} True if the value matches the glob, false otherwise. + */ + test(val: string): boolean { + return this._regex.test(val); + } + +} diff --git a/yarn.lock b/yarn.lock index aa0a06e588..a2effb975c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3674,6 +3674,11 @@ glob-to-regexp@^0.3.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + glob@7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" From e9c8a31e1f07031e1b315020d48bb97434f40f41 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 13:28:00 -0600 Subject: [PATCH 03/11] Start and stop Mjolnir with the lifecycle --- src/Lifecycle.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 13f3abccb1..f2b50d7f2d 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -36,6 +36,7 @@ import * as StorageManager from './utils/StorageManager'; import SettingsStore from "./settings/SettingsStore"; import TypingStore from "./stores/TypingStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; +import {Mjolnir} from "./mjolnir/Mjolnir"; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -585,6 +586,11 @@ async function startMatrixClient(startSyncing=true) { IntegrationManagers.sharedInstance().startWatching(); ActiveWidgetStore.start(); + // Start Mjolnir even though we haven't checked the feature flag yet. Starting + // the thing just wastes CPU cycles, but should result in no actual functionality + // being exposed to the user. + Mjolnir.sharedInstance().start(); + if (startSyncing) { await MatrixClientPeg.start(); } else { @@ -645,6 +651,7 @@ export function stopMatrixClient(unsetClient=true) { Presence.stop(); ActiveWidgetStore.stop(); IntegrationManagers.sharedInstance().stopWatching(); + Mjolnir.sharedInstance().stop(); if (DMRoomMap.shared()) DMRoomMap.shared().stop(); const cli = MatrixClientPeg.get(); if (cli) { From b93508728a1e4abd3dd8fa411eb6760119bf6f7d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 14:24:51 -0600 Subject: [PATCH 04/11] Add personal list management to Mjolnir section --- res/css/_components.scss | 1 + .../tabs/user/_MjolnirUserSettingsTab.scss | 23 ++++ .../tabs/user/MjolnirUserSettingsTab.js | 117 +++++++++++++++++- src/i18n/strings/en_EN.json | 11 +- src/mjolnir/BanList.js | 16 ++- src/mjolnir/Mjolnir.js | 44 ++++++- src/utils/MatrixGlob.js | 2 +- 7 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index 29c4d2c84c..a0e5881201 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -182,6 +182,7 @@ @import "./views/settings/tabs/room/_SecurityRoomSettingsTab.scss"; @import "./views/settings/tabs/user/_GeneralUserSettingsTab.scss"; @import "./views/settings/tabs/user/_HelpUserSettingsTab.scss"; +@import "./views/settings/tabs/user/_MjolnirUserSettingsTab.scss"; @import "./views/settings/tabs/user/_NotificationUserSettingsTab.scss"; @import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss"; @import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss"; diff --git a/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss b/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss new file mode 100644 index 0000000000..930dbeb440 --- /dev/null +++ b/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss @@ -0,0 +1,23 @@ +/* +Copyright 2019 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_MjolnirUserSettingsTab .mx_Field { + @mixin mx_Settings_fullWidthField; +} + +.mx_MjolnirUserSettingsTab_personalRule { + margin-bottom: 2px; +} diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js index 02e64c0bc1..97f92bb0b2 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js @@ -16,28 +16,115 @@ limitations under the License. import React from 'react'; import {_t} from "../../../../../languageHandler"; +import {Mjolnir} from "../../../../../mjolnir/Mjolnir"; +import {ListRule} from "../../../../../mjolnir/ListRule"; +import {RULE_SERVER, RULE_USER} from "../../../../../mjolnir/BanList"; +import Modal from "../../../../../Modal"; + const sdk = require("../../../../.."); export default class MjolnirUserSettingsTab extends React.Component { constructor() { super(); + + this.state = { + busy: false, + newPersonalRule: "", + }; + } + + _onPersonalRuleChanged = (e) => { + this.setState({newPersonalRule: e.target.value}); + }; + + _onAddPersonalRule = async (e) => { + e.preventDefault(); + e.stopPropagation(); + + let kind = RULE_SERVER; + if (this.state.newPersonalRule.startsWith("@")) { + kind = RULE_USER; + } + + this.setState({busy: true}); + try { + const list = await Mjolnir.sharedInstance().getOrCreatePersonalList(); + await list.banEntity(kind, this.state.newPersonalRule, _t("Ignored/Blocked")); + this.setState({newPersonalRule: ""}); // this will also cause the new rule to be rendered + } catch (e) { + console.error(e); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to remove Mjolnir rule', '', ErrorDialog, { + title: _t('Error removing ignored user/server'), + description: _t('Something went wrong. Please try again or view your console for hints.'), + }); + } finally { + this.setState({busy: false}); + } + }; + + async _removePersonalRule(rule: ListRule) { + this.setState({busy: true}); + try { + const list = Mjolnir.sharedInstance().getPersonalList(); + await list.unbanEntity(rule.kind, rule.entity); + } catch (e) { + console.error(e); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to remove Mjolnir rule', '', ErrorDialog, { + title: _t('Error removing ignored user/server'), + description: _t('Something went wrong. Please try again or view your console for hints.'), + }); + } finally { + this.setState({busy: false}); + } + } + + _renderPersonalBanListRules() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + const list = Mjolnir.sharedInstance().getPersonalList(); + const rules = list ? [...list.userRules, ...list.serverRules] : []; + if (!list || rules.length <= 0) return {_t("You have not ignored anyone.")}; + + const tiles = []; + for (const rule of rules) { + tiles.push( +
  • + this._removePersonalRule(rule)} + disabled={this.state.busy}> + {_t("Remove")} +   + {rule.entity} +
  • , + ); + } + + return

    {_t("You are currently ignoring:")}

    +
      {tiles}
    +
    ; } render() { + const Field = sdk.getComponent('elements.Field'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return ( -
    +
    {_t("Ignored users")}
    - {_t("⚠ These settings are meant for advanced users.")}
    -
    + {_t("⚠ These settings are meant for advanced users.")}
    +
    {_t( "Add users and servers you want to ignore here. Use asterisks " + "to have Riot match any characters. For example, @bot:* " + "would ignore all users that have the name 'bot' on any server.", {}, {code: (s) => {s}}, - )}
    -
    + )}
    +
    {_t( "Ignoring people is done through ban lists which contain rules for " + "who to ban. Subscribing to a ban list means the users/servers blocked by " + @@ -55,7 +142,25 @@ export default class MjolnirUserSettingsTab extends React.Component { "the ban list in effect.", )}
    -

    TODO

    +
    + {this._renderPersonalBanListRules()} +
    +
    +
    + + + {_t("Ignore")} + + +
    {_t("Subscribed lists")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 770f4723ef..fa15433a1a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -640,12 +640,21 @@ "Access Token:": "Access Token:", "click to reveal": "click to reveal", "Labs": "Labs", + "Ignored/Blocked": "Ignored/Blocked", + "Error removing ignored user/server": "Error removing ignored user/server", + "Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.", + "You have not ignored anyone.": "You have not ignored anyone.", + "Remove": "Remove", + "You are currently ignoring:": "You are currently ignoring:", "Ignored users": "Ignored users", "⚠ These settings are meant for advanced users.": "⚠ These settings are meant for advanced users.", "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.", "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.", "Personal ban list": "Personal ban list", "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.", + "Server or user ID to ignore": "Server or user ID to ignore", + "eg: @bot:* or example.org": "eg: @bot:* or example.org", + "Ignore": "Ignore", "Subscribed lists": "Subscribed lists", "Subscribing to a ban list will cause you to join it!": "Subscribing to a ban list will cause you to join it!", "If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.", @@ -776,7 +785,6 @@ "Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.", "Unable to remove contact information": "Unable to remove contact information", "Remove %(email)s?": "Remove %(email)s?", - "Remove": "Remove", "Invalid Email Address": "Invalid Email Address", "This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address", "Unable to add email address": "Unable to add email address", @@ -843,7 +851,6 @@ "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", "Are you sure?": "Are you sure?", "No devices with registered encryption keys": "No devices with registered encryption keys", - "Ignore": "Ignore", "Jump to read receipt": "Jump to read receipt", "Mention": "Mention", "Invite": "Invite", diff --git a/src/mjolnir/BanList.js b/src/mjolnir/BanList.js index 6ebc0a7e36..026005420a 100644 --- a/src/mjolnir/BanList.js +++ b/src/mjolnir/BanList.js @@ -60,17 +60,23 @@ export class BanList { return this._rules.filter(r => r.kind === RULE_ROOM); } - banEntity(kind: string, entity: string, reason: string): Promise { - return MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), { + async banEntity(kind: string, entity: string, reason: string): Promise { + await MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), { entity: entity, reason: reason, recommendation: recommendationToStable(RECOMMENDATION_BAN, true), }, "rule:" + entity); + this._rules.push(new ListRule(entity, RECOMMENDATION_BAN, reason, ruleTypeToStable(kind, false))); } - unbanEntity(kind: string, entity: string): Promise { + async unbanEntity(kind: string, entity: string): Promise { // Empty state event is effectively deleting it. - return MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), {}, "rule:" + entity); + await MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), {}, "rule:" + entity); + this._rules = this._rules.filter(r => { + if (r.kind !== ruleTypeToStable(kind, false)) return true; + if (r.entity !== entity) return true; + return false; // we just deleted this rule + }); } updateList() { @@ -82,7 +88,7 @@ export class BanList { for (const eventType of ALL_RULE_TYPES) { const events = room.currentState.getStateEvents(eventType, undefined); for (const ev of events) { - if (!ev['state_key']) continue; + if (!ev.getStateKey()) continue; const kind = ruleTypeToStable(eventType, false); diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.js index a12534592d..d90ea9cd04 100644 --- a/src/mjolnir/Mjolnir.js +++ b/src/mjolnir/Mjolnir.js @@ -18,6 +18,7 @@ import MatrixClientPeg from "../MatrixClientPeg"; import {ALL_RULE_TYPES, BanList} from "./BanList"; import SettingsStore, {SettingLevel} from "../settings/SettingsStore"; import {_t} from "../languageHandler"; +import dis from "../dispatcher"; // TODO: Move this and related files to the js-sdk or something once finalized. @@ -27,19 +28,39 @@ export class Mjolnir { _lists: BanList[] = []; _roomIds: string[] = []; _mjolnirWatchRef = null; + _dispatcherRef = null; constructor() { } start() { - this._updateLists(SettingsStore.getValue("mjolnirRooms")); this._mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this._onListsChanged.bind(this)); + this._dispatcherRef = dis.register(this._onAction); + dis.dispatch({ + action: 'do_after_sync_prepared', + deferred_action: {action: 'setup_mjolnir'}, + }); + } + + _onAction = (payload) => { + if (payload['action'] === 'setup_mjolnir') { + console.log("Setting up Mjolnir: after sync"); + this.setup(); + } + }; + + setup() { + if (!MatrixClientPeg.get()) return; + this._updateLists(SettingsStore.getValue("mjolnirRooms")); MatrixClientPeg.get().on("RoomState.events", this._onEvent.bind(this)); } stop() { SettingsStore.unwatchSetting(this._mjolnirWatchRef); + dis.unregister(this._dispatcherRef); + + if (!MatrixClientPeg.get()) return; MatrixClientPeg.get().removeListener("RoomState.events", this._onEvent.bind(this)); } @@ -52,8 +73,8 @@ export class Mjolnir { preset: "private_chat" }); personalRoomId = resp['room_id']; - SettingsStore.setValue("mjolnirPersonalRoom", null, SettingLevel.ACCOUNT, personalRoomId); - SettingsStore.setValue("mjolnirRooms", null, SettingLevel.ACCOUNT, [personalRoomId, ...this._roomIds]); + await SettingsStore.setValue("mjolnirPersonalRoom", null, SettingLevel.ACCOUNT, personalRoomId); + await SettingsStore.setValue("mjolnirRooms", null, SettingLevel.ACCOUNT, [personalRoomId, ...this._roomIds]); } if (!personalRoomId) { throw new Error("Error finding a room ID to use"); @@ -67,7 +88,21 @@ export class Mjolnir { return list; } + // get without creating the list + getPersonalList(): BanList { + const personalRoomId = SettingsStore.getValue("mjolnirPersonalRoom"); + if (!personalRoomId) return null; + + let list = this._lists.find(b => b.roomId === personalRoomId); + if (!list) list = new BanList(personalRoomId); + // we don't append the list to the tracked rooms because it should already be there. + // we're just trying to get the caller some utility access to the list + + return list; + } + _onEvent(event) { + if (!MatrixClientPeg.get()) return; if (!this._roomIds.includes(event.getRoomId())) return; if (!ALL_RULE_TYPES.includes(event.getType())) return; @@ -80,6 +115,9 @@ export class Mjolnir { } _updateLists(listRoomIds: string[]) { + if (!MatrixClientPeg.get()) return; + + console.log("Updating Mjolnir ban lists to: " + listRoomIds); this._lists = []; this._roomIds = listRoomIds || []; if (!listRoomIds) return; diff --git a/src/utils/MatrixGlob.js b/src/utils/MatrixGlob.js index cf55040625..b18e20ecf4 100644 --- a/src/utils/MatrixGlob.js +++ b/src/utils/MatrixGlob.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as globToRegexp from "glob-to-regexp"; +import globToRegexp from "glob-to-regexp"; // Taken with permission from matrix-bot-sdk: // https://github.com/turt2live/matrix-js-bot-sdk/blob/eb148c2ecec7bf3ade801d73deb43df042d55aef/src/MatrixGlob.ts From 39b657ce7c4c3402802c836acce8d2c095c0bb9a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 15:53:18 -0600 Subject: [PATCH 05/11] Add basic structure for (un)subscribing from lists --- .../tabs/user/_MjolnirUserSettingsTab.scss | 2 +- .../tabs/user/MjolnirUserSettingsTab.js | 145 ++++++++++++++++-- src/i18n/strings/en_EN.json | 13 +- src/mjolnir/Mjolnir.js | 20 +++ 4 files changed, 166 insertions(+), 14 deletions(-) diff --git a/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss b/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss index 930dbeb440..c60cbc5dea 100644 --- a/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss @@ -18,6 +18,6 @@ limitations under the License. @mixin mx_Settings_fullWidthField; } -.mx_MjolnirUserSettingsTab_personalRule { +.mx_MjolnirUserSettingsTab_listItem { margin-bottom: 2px; } diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js index 97f92bb0b2..4e05b57567 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js @@ -18,8 +18,9 @@ import React from 'react'; import {_t} from "../../../../../languageHandler"; import {Mjolnir} from "../../../../../mjolnir/Mjolnir"; import {ListRule} from "../../../../../mjolnir/ListRule"; -import {RULE_SERVER, RULE_USER} from "../../../../../mjolnir/BanList"; +import {BanList, RULE_SERVER, RULE_USER} from "../../../../../mjolnir/BanList"; import Modal from "../../../../../Modal"; +import MatrixClientPeg from "../../../../../MatrixClientPeg"; const sdk = require("../../../../.."); @@ -30,6 +31,7 @@ export default class MjolnirUserSettingsTab extends React.Component { this.state = { busy: false, newPersonalRule: "", + newList: "", }; } @@ -37,6 +39,10 @@ export default class MjolnirUserSettingsTab extends React.Component { this.setState({newPersonalRule: e.target.value}); }; + _onNewListChanged = (e) => { + this.setState({newList: e.target.value}); + }; + _onAddPersonalRule = async (e) => { e.preventDefault(); e.stopPropagation(); @@ -55,8 +61,8 @@ export default class MjolnirUserSettingsTab extends React.Component { console.error(e); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to remove Mjolnir rule', '', ErrorDialog, { - title: _t('Error removing ignored user/server'), + Modal.createTrackedDialog('Failed to add Mjolnir rule', '', ErrorDialog, { + title: _t('Error adding ignored user/server'), description: _t('Something went wrong. Please try again or view your console for hints.'), }); } finally { @@ -64,6 +70,28 @@ export default class MjolnirUserSettingsTab extends React.Component { } }; + _onSubscribeList = async (e) => { + e.preventDefault(); + e.stopPropagation(); + + this.setState({busy: true}); + try { + const room = await MatrixClientPeg.get().joinRoom(this.state.newList); + await Mjolnir.sharedInstance().subscribeToList(room.roomId); + this.setState({newList: ""}); // this will also cause the new rule to be rendered + } catch (e) { + console.error(e); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to subscribe to Mjolnir list', '', ErrorDialog, { + title: _t('Error subscribing to list'), + description: _t('Please verify the room ID or alias and try again.'), + }); + } finally { + this.setState({busy: false}); + } + }; + async _removePersonalRule(rule: ListRule) { this.setState({busy: true}); try { @@ -82,6 +110,28 @@ export default class MjolnirUserSettingsTab extends React.Component { } } + async _unsubscribeFromList(list: BanList) { + this.setState({busy: true}); + try { + await Mjolnir.sharedInstance().unsubscribeFromList(list.roomId); + await MatrixClientPeg.get().leave(list.roomId); + } catch (e) { + console.error(e); + + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to unsubscribe from Mjolnir list', '', ErrorDialog, { + title: _t('Error unsubscribing from list'), + description: _t('Please try again or view your console for hints.'), + }); + } finally { + this.setState({busy: false}); + } + } + + _viewListRules(list: BanList) { + // TODO + } + _renderPersonalBanListRules() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); @@ -92,9 +142,12 @@ export default class MjolnirUserSettingsTab extends React.Component { const tiles = []; for (const rule of rules) { tiles.push( -
  • - this._removePersonalRule(rule)} - disabled={this.state.busy}> +
  • + this._removePersonalRule(rule)} + disabled={this.state.busy} + > {_t("Remove")}   {rule.entity} @@ -102,9 +155,52 @@ export default class MjolnirUserSettingsTab extends React.Component { ); } - return

    {_t("You are currently ignoring:")}

    -
      {tiles}
    -
    ; + return ( +
    +

    {_t("You are currently ignoring:")}

    +
      {tiles}
    +
    + ); + } + + _renderSubscribedBanLists() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + const personalList = Mjolnir.sharedInstance().getPersonalList(); + const lists = Mjolnir.sharedInstance().lists.filter(b => personalList ? personalList.roomId !== b.roomId : true); + if (!lists || lists.length <= 0) return {_t("You are not subscribed to any lists")}; + + const tiles = []; + for (const list of lists) { + const room = MatrixClientPeg.get().getRoom(list.roomId); + const name = room ? {room.name} ({list.roomId}) : list.roomId; + tiles.push( +
  • + this._unsubscribeFromList(list)} + disabled={this.state.busy} + > + {_t("Unsubscribe")} +   + this._viewListRules(list)} + disabled={this.state.busy} + > + {_t("View rules")} +   + {name} +
  • , + ); + } + + return ( +
    +

    {_t("You are currently subscribed to:")}

    +
      {tiles}
    +
    + ); } render() { @@ -155,8 +251,12 @@ export default class MjolnirUserSettingsTab extends React.Component { value={this.state.newPersonalRule} onChange={this._onPersonalRuleChanged} /> - + {_t("Ignore")} @@ -171,7 +271,28 @@ export default class MjolnirUserSettingsTab extends React.Component { "If this isn't what you want, please use a different tool to ignore users.", )}
    -

    TODO

    +
    + {this._renderSubscribedBanLists()} +
    +
    +
    + + + {_t("Subscribe")} + + +
    ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fa15433a1a..561dbc4da9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -641,11 +641,20 @@ "click to reveal": "click to reveal", "Labs": "Labs", "Ignored/Blocked": "Ignored/Blocked", - "Error removing ignored user/server": "Error removing ignored user/server", + "Error adding ignored user/server": "Error adding ignored user/server", "Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.", + "Error subscribing to list": "Error subscribing to list", + "Please verify the room ID or alias and try again.": "Please verify the room ID or alias and try again.", + "Error removing ignored user/server": "Error removing ignored user/server", + "Error unsubscribing from list": "Error unsubscribing from list", + "Please try again or view your console for hints.": "Please try again or view your console for hints.", "You have not ignored anyone.": "You have not ignored anyone.", "Remove": "Remove", "You are currently ignoring:": "You are currently ignoring:", + "You are not subscribed to any lists": "You are not subscribed to any lists", + "Unsubscribe": "Unsubscribe", + "View rules": "View rules", + "You are currently subscribed to:": "You are currently subscribed to:", "Ignored users": "Ignored users", "⚠ These settings are meant for advanced users.": "⚠ These settings are meant for advanced users.", "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Add users and servers you want to ignore here. Use asterisks to have Riot match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.", @@ -658,6 +667,8 @@ "Subscribed lists": "Subscribed lists", "Subscribing to a ban list will cause you to join it!": "Subscribing to a ban list will cause you to join it!", "If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.", + "Room ID or alias of ban list": "Room ID or alias of ban list", + "Subscribe": "Subscribe", "Notifications": "Notifications", "Start automatically after system login": "Start automatically after system login", "Always show the window menu bar": "Always show the window menu bar", diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.js index d90ea9cd04..5edfe3750e 100644 --- a/src/mjolnir/Mjolnir.js +++ b/src/mjolnir/Mjolnir.js @@ -33,6 +33,14 @@ export class Mjolnir { constructor() { } + get roomIds(): string[] { + return this._roomIds; + } + + get lists(): BanList[] { + return this._lists; + } + start() { this._mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this._onListsChanged.bind(this)); @@ -101,6 +109,18 @@ export class Mjolnir { return list; } + async subscribeToList(roomId: string) { + const roomIds = [...this._roomIds, roomId]; + await SettingsStore.setValue("mjolnirRooms", null, SettingLevel.ACCOUNT, roomIds); + this._lists.push(new BanList(roomId)); + } + + async unsubscribeFromList(roomId: string) { + const roomIds = this._roomIds.filter(r => r !== roomId); + await SettingsStore.setValue("mjolnirRooms", null, SettingLevel.ACCOUNT, roomIds); + this._lists = this._lists.filter(b => b.roomId !== roomId); + } + _onEvent(event) { if (!MatrixClientPeg.get()) return; if (!this._roomIds.includes(event.getRoomId())) return; From b420fd675857d6c3e212caafa1c56d2ddc4a16da Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 16:00:31 -0600 Subject: [PATCH 06/11] Add a view rules dialog --- .../tabs/user/MjolnirUserSettingsTab.js | 29 ++++++++++++++++++- src/i18n/strings/en_EN.json | 6 +++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js index 4e05b57567..a02ca2c570 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js @@ -129,7 +129,34 @@ export default class MjolnirUserSettingsTab extends React.Component { } _viewListRules(list: BanList) { - // TODO + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + + const room = MatrixClientPeg.get().getRoom(list.roomId); + const name = room ? room.name : list.roomId; + + const renderRules = (rules: ListRule[]) => { + if (rules.length === 0) return {_t("None")}; + + const tiles = []; + for (const rule of rules) { + tiles.push(
  • {rule.entity}
  • ); + } + return
      {tiles}
    ; + }; + + Modal.createTrackedDialog('View Mjolnir list rules', '', QuestionDialog, { + title: _t("Ban list rules - %(roomName)s", {roomName: name}), + description: ( +
    +

    {_t("Server rules")}

    + {renderRules(list.serverRules)} +

    {_t("User rules")}

    + {renderRules(list.userRules)} +
    + ), + button: _t("Close"), + hasCancelButton: false, + }); } _renderPersonalBanListRules() { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 561dbc4da9..58fa564250 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -648,6 +648,11 @@ "Error removing ignored user/server": "Error removing ignored user/server", "Error unsubscribing from list": "Error unsubscribing from list", "Please try again or view your console for hints.": "Please try again or view your console for hints.", + "None": "None", + "Ban list rules - %(roomName)s": "Ban list rules - %(roomName)s", + "Server rules": "Server rules", + "User rules": "User rules", + "Close": "Close", "You have not ignored anyone.": "You have not ignored anyone.", "Remove": "Remove", "You are currently ignoring:": "You are currently ignoring:", @@ -874,7 +879,6 @@ "Revoke Moderator": "Revoke Moderator", "Make Moderator": "Make Moderator", "Admin Tools": "Admin Tools", - "Close": "Close", "and %(count)s others...|other": "and %(count)s others...", "and %(count)s others...|one": "and one other...", "Invite to this room": "Invite to this room", From 11068d189cf03e309cccca75b83ee8674fb01796 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 16:19:42 -0600 Subject: [PATCH 07/11] Hide messages blocked by ban lists --- res/css/_components.scss | 1 + res/css/views/messages/_MjolnirBody.scss | 19 ++++++++ src/components/views/messages/MessageEvent.js | 24 +++++++++- src/components/views/messages/MjolnirBody.js | 47 +++++++++++++++++++ src/i18n/strings/en_EN.json | 1 + 5 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 res/css/views/messages/_MjolnirBody.scss create mode 100644 src/components/views/messages/MjolnirBody.js diff --git a/res/css/_components.scss b/res/css/_components.scss index a0e5881201..788e22a766 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -123,6 +123,7 @@ @import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; +@import "./views/messages/_MjolnirBody.scss"; @import "./views/messages/_ReactionsRow.scss"; @import "./views/messages/_ReactionsRowButton.scss"; @import "./views/messages/_ReactionsRowButtonTooltip.scss"; diff --git a/res/css/views/messages/_MjolnirBody.scss b/res/css/views/messages/_MjolnirBody.scss new file mode 100644 index 0000000000..80be7429e5 --- /dev/null +++ b/res/css/views/messages/_MjolnirBody.scss @@ -0,0 +1,19 @@ +/* +Copyright 2019 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_MjolnirBody { + opacity: 0.4; +} diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index a616dd96ed..2e353794d7 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -18,6 +18,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import sdk from '../../../index'; +import SettingsStore from "../../../settings/SettingsStore"; +import {Mjolnir} from "../../../mjolnir/Mjolnir"; module.exports = createReactClass({ displayName: 'MessageEvent', @@ -49,6 +51,10 @@ module.exports = createReactClass({ return this.refs.body && this.refs.body.getEventTileOps ? this.refs.body.getEventTileOps() : null; }, + onTileUpdate: function() { + this.forceUpdate(); + }, + render: function() { const UnknownBody = sdk.getComponent('messages.UnknownBody'); @@ -81,6 +87,20 @@ module.exports = createReactClass({ } } + if (SettingsStore.isFeatureEnabled("feature_mjolnir")) { + const allowRender = localStorage.getItem(`mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`) === "true"; + + if (!allowRender) { + const userDomain = this.props.mxEvent.getSender().split(':').slice(1).join(':'); + const userBanned = Mjolnir.sharedInstance().isUserBanned(this.props.mxEvent.getSender()); + const serverBanned = Mjolnir.sharedInstance().isServerBanned(userDomain); + + if (userBanned || serverBanned) { + BodyType = sdk.getComponent('messages.MjolnirBody'); + } + } + } + return ; + onHeightChanged={this.props.onHeightChanged} + onTileUpdate={this.onTileUpdate} + />; }, }); diff --git a/src/components/views/messages/MjolnirBody.js b/src/components/views/messages/MjolnirBody.js new file mode 100644 index 0000000000..994642863b --- /dev/null +++ b/src/components/views/messages/MjolnirBody.js @@ -0,0 +1,47 @@ +/* +Copyright 2019 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 PropTypes from 'prop-types'; +import {_t} from '../../../languageHandler'; + +export default class MjolnirBody extends React.Component { + static propTypes = { + mxEvent: PropTypes.object.isRequired, + onTileUpdate: PropTypes.func.isRequired, + }; + + constructor() { + super(); + } + + _onAllowClick = (e) => { + e.preventDefault(); + e.stopPropagation(); + + localStorage.setItem(`mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`, "true"); + this.props.onTileUpdate(); + }; + + render() { + return ( +
    {_t( + "You have ignored this user, so their message is hidden. Show anyways.", + {}, {a: (sub) => {sub}}, + )}
    + ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 58fa564250..74433a9c04 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1094,6 +1094,7 @@ "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Show image": "Show image", + "You have ignored this user, so their message is hidden. Show anyways.": "You have ignored this user, so their message is hidden. Show anyways.", "Error decrypting video": "Error decrypting video", "Show all": "Show all", "reacted with %(shortName)s": "reacted with %(shortName)s", From 3e4a721111f6bb6a17e219ea97ead4dfe4589792 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 16:27:45 -0600 Subject: [PATCH 08/11] Appease the linter --- src/components/views/dialogs/UserSettingsDialog.js | 2 +- src/components/views/messages/MessageEvent.js | 3 ++- src/components/views/messages/MjolnirBody.js | 3 ++- .../settings/tabs/user/MjolnirUserSettingsTab.js | 14 ++++++++------ src/mjolnir/BanList.js | 12 +++++++++--- src/mjolnir/ListRule.js | 4 +++- src/mjolnir/Mjolnir.js | 8 +++++--- src/utils/MatrixGlob.js | 1 - 8 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index 6e324ad3fb..d3ab2b8722 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -42,7 +42,7 @@ export default class UserSettingsDialog extends React.Component { this.state = { mjolnirEnabled: SettingsStore.isFeatureEnabled("feature_mjolnir"), - } + }; } componentDidMount(): void { diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 2e353794d7..0d22658884 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -88,7 +88,8 @@ module.exports = createReactClass({ } if (SettingsStore.isFeatureEnabled("feature_mjolnir")) { - const allowRender = localStorage.getItem(`mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`) === "true"; + const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`; + const allowRender = localStorage.getItem(key) === "true"; if (!allowRender) { const userDomain = this.props.mxEvent.getSender().split(':').slice(1).join(':'); diff --git a/src/components/views/messages/MjolnirBody.js b/src/components/views/messages/MjolnirBody.js index 994642863b..d03c6c658d 100644 --- a/src/components/views/messages/MjolnirBody.js +++ b/src/components/views/messages/MjolnirBody.js @@ -32,7 +32,8 @@ export default class MjolnirBody extends React.Component { e.preventDefault(); e.stopPropagation(); - localStorage.setItem(`mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`, "true"); + const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`; + localStorage.setItem(key, "true"); this.props.onTileUpdate(); }; diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js index a02ca2c570..608be0b129 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js @@ -194,7 +194,9 @@ export default class MjolnirUserSettingsTab extends React.Component { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const personalList = Mjolnir.sharedInstance().getPersonalList(); - const lists = Mjolnir.sharedInstance().lists.filter(b => personalList ? personalList.roomId !== b.roomId : true); + const lists = Mjolnir.sharedInstance().lists.filter(b => { + return personalList? personalList.roomId !== b.roomId : true; + }); if (!lists || lists.length <= 0) return {_t("You are not subscribed to any lists")}; const tiles = []; @@ -239,19 +241,19 @@ export default class MjolnirUserSettingsTab extends React.Component {
    {_t("Ignored users")}
    - {_t("⚠ These settings are meant for advanced users.")}
    -
    + {_t("⚠ These settings are meant for advanced users.")}
    +
    {_t( "Add users and servers you want to ignore here. Use asterisks " + "to have Riot match any characters. For example, @bot:* " + "would ignore all users that have the name 'bot' on any server.", {}, {code: (s) => {s}}, - )}
    -
    + )}
    +
    {_t( "Ignoring people is done through ban lists which contain rules for " + "who to ban. Subscribing to a ban list means the users/servers blocked by " + - "that list will be hidden from you." + "that list will be hidden from you.", )}
    diff --git a/src/mjolnir/BanList.js b/src/mjolnir/BanList.js index 026005420a..60a924a52b 100644 --- a/src/mjolnir/BanList.js +++ b/src/mjolnir/BanList.js @@ -29,9 +29,15 @@ export const SERVER_RULE_TYPES = [RULE_SERVER, "org.matrix.mjolnir.rule.server"] export const ALL_RULE_TYPES = [...USER_RULE_TYPES, ...ROOM_RULE_TYPES, ...SERVER_RULE_TYPES]; export function ruleTypeToStable(rule: string, unstable = true): string { - if (USER_RULE_TYPES.includes(rule)) return unstable ? USER_RULE_TYPES[USER_RULE_TYPES.length - 1] : RULE_USER; - if (ROOM_RULE_TYPES.includes(rule)) return unstable ? ROOM_RULE_TYPES[ROOM_RULE_TYPES.length - 1] : RULE_ROOM; - if (SERVER_RULE_TYPES.includes(rule)) return unstable ? SERVER_RULE_TYPES[SERVER_RULE_TYPES.length - 1] : RULE_SERVER; + if (USER_RULE_TYPES.includes(rule)) { + return unstable ? USER_RULE_TYPES[USER_RULE_TYPES.length - 1] : RULE_USER; + } + if (ROOM_RULE_TYPES.includes(rule)) { + return unstable ? ROOM_RULE_TYPES[ROOM_RULE_TYPES.length - 1] : RULE_ROOM; + } + if (SERVER_RULE_TYPES.includes(rule)) { + return unstable ? SERVER_RULE_TYPES[SERVER_RULE_TYPES.length - 1] : RULE_SERVER; + } return null; } diff --git a/src/mjolnir/ListRule.js b/src/mjolnir/ListRule.js index d33248d24c..1d472e06d6 100644 --- a/src/mjolnir/ListRule.js +++ b/src/mjolnir/ListRule.js @@ -22,7 +22,9 @@ export const RECOMMENDATION_BAN = "m.ban"; export const RECOMMENDATION_BAN_TYPES = [RECOMMENDATION_BAN, "org.matrix.mjolnir.ban"]; export function recommendationToStable(recommendation: string, unstable = true): string { - if (RECOMMENDATION_BAN_TYPES.includes(recommendation)) return unstable ? RECOMMENDATION_BAN_TYPES[RECOMMENDATION_BAN_TYPES.length - 1] : RECOMMENDATION_BAN; + if (RECOMMENDATION_BAN_TYPES.includes(recommendation)) { + return unstable ? RECOMMENDATION_BAN_TYPES[RECOMMENDATION_BAN_TYPES.length - 1] : RECOMMENDATION_BAN; + } return null; } diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.js index 5edfe3750e..9177c621d1 100644 --- a/src/mjolnir/Mjolnir.js +++ b/src/mjolnir/Mjolnir.js @@ -78,11 +78,13 @@ export class Mjolnir { const resp = await MatrixClientPeg.get().createRoom({ name: _t("My Ban List"), topic: _t("This is your list of users/servers you have blocked - don't leave the room!"), - preset: "private_chat" + preset: "private_chat", }); personalRoomId = resp['room_id']; - await SettingsStore.setValue("mjolnirPersonalRoom", null, SettingLevel.ACCOUNT, personalRoomId); - await SettingsStore.setValue("mjolnirRooms", null, SettingLevel.ACCOUNT, [personalRoomId, ...this._roomIds]); + await SettingsStore.setValue( + "mjolnirPersonalRoom", null, SettingLevel.ACCOUNT, personalRoomId); + await SettingsStore.setValue( + "mjolnirRooms", null, SettingLevel.ACCOUNT, [personalRoomId, ...this._roomIds]); } if (!personalRoomId) { throw new Error("Error finding a room ID to use"); diff --git a/src/utils/MatrixGlob.js b/src/utils/MatrixGlob.js index b18e20ecf4..e07aaab541 100644 --- a/src/utils/MatrixGlob.js +++ b/src/utils/MatrixGlob.js @@ -50,5 +50,4 @@ export class MatrixGlob { test(val: string): boolean { return this._regex.test(val); } - } From 3c45a39caaab2c13f8b687e08679ead3adca7b85 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 16:30:51 -0600 Subject: [PATCH 09/11] Appease the other linter --- res/css/views/messages/_MjolnirBody.scss | 2 +- res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/res/css/views/messages/_MjolnirBody.scss b/res/css/views/messages/_MjolnirBody.scss index 80be7429e5..2760adfd7e 100644 --- a/res/css/views/messages/_MjolnirBody.scss +++ b/res/css/views/messages/_MjolnirBody.scss @@ -15,5 +15,5 @@ limitations under the License. */ .mx_MjolnirBody { - opacity: 0.4; + opacity: 0.4; } diff --git a/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss b/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss index c60cbc5dea..2a3fd12f31 100644 --- a/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_MjolnirUserSettingsTab.scss @@ -15,9 +15,9 @@ limitations under the License. */ .mx_MjolnirUserSettingsTab .mx_Field { - @mixin mx_Settings_fullWidthField; + @mixin mx_Settings_fullWidthField; } .mx_MjolnirUserSettingsTab_listItem { - margin-bottom: 2px; + margin-bottom: 2px; } From 07b8e128d2adc198767d9978329448ea59dad868 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 16:43:03 -0600 Subject: [PATCH 10/11] Bypass the tests being weird They run kinda-but-not-really async, which can lead to early/late calls to `stop()` --- src/mjolnir/Mjolnir.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.js index 9177c621d1..7539dfafb0 100644 --- a/src/mjolnir/Mjolnir.js +++ b/src/mjolnir/Mjolnir.js @@ -66,7 +66,14 @@ export class Mjolnir { stop() { SettingsStore.unwatchSetting(this._mjolnirWatchRef); - dis.unregister(this._dispatcherRef); + + try { + if (this._dispatcherRef) dis.unregister(this._dispatcherRef); + } catch (e) { + console.error(e); + // Only the tests cause problems with this particular block of code. We should + // never be here in production. + } if (!MatrixClientPeg.get()) return; MatrixClientPeg.get().removeListener("RoomState.events", this._onEvent.bind(this)); From 86be607e92dbf148498e284d083e62b8716be2a8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 6 Nov 2019 10:52:00 -0700 Subject: [PATCH 11/11] onTileUpdate -> onMessageAllowed We keep onTileUpdate in MessgeEvent because it's a generic thing for the class to handle. onMessageAllowed is slightly different than onShowAllowed because "show allowed" doesn't quite make sense on its own, imo. --- src/components/views/messages/MessageEvent.js | 2 +- src/components/views/messages/MjolnirBody.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 0d22658884..e75bcc4332 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -112,7 +112,7 @@ module.exports = createReactClass({ replacingEventId={this.props.replacingEventId} editState={this.props.editState} onHeightChanged={this.props.onHeightChanged} - onTileUpdate={this.onTileUpdate} + onMessageAllowed={this.onTileUpdate} />; }, }); diff --git a/src/components/views/messages/MjolnirBody.js b/src/components/views/messages/MjolnirBody.js index d03c6c658d..baaee91657 100644 --- a/src/components/views/messages/MjolnirBody.js +++ b/src/components/views/messages/MjolnirBody.js @@ -21,7 +21,7 @@ import {_t} from '../../../languageHandler'; export default class MjolnirBody extends React.Component { static propTypes = { mxEvent: PropTypes.object.isRequired, - onTileUpdate: PropTypes.func.isRequired, + onMessageAllowed: PropTypes.func.isRequired, }; constructor() { @@ -34,7 +34,7 @@ export default class MjolnirBody extends React.Component { const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`; localStorage.setItem(key, "true"); - this.props.onTileUpdate(); + this.props.onMessageAllowed(); }; render() {