From b93508728a1e4abd3dd8fa411eb6760119bf6f7d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 31 Oct 2019 14:24:51 -0600 Subject: [PATCH] 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:")}

    + +
    ; } 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