Add personal list management to Mjolnir section

This commit is contained in:
Travis Ralston 2019-10-31 14:24:51 -06:00
parent e9c8a31e1f
commit b93508728a
7 changed files with 197 additions and 17 deletions

View file

@ -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";

View file

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

View file

@ -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 <i>{_t("You have not ignored anyone.")}</i>;
const tiles = [];
for (const rule of rules) {
tiles.push(
<li key={rule.entity} className="mx_MjolnirUserSettingsTab_personalRule">
<AccessibleButton kind="danger_sm" onClick={() => this._removePersonalRule(rule)}
disabled={this.state.busy}>
{_t("Remove")}
</AccessibleButton>&nbsp;
<code>{rule.entity}</code>
</li>,
);
}
return <div><p>{_t("You are currently ignoring:")}</p>
<ul>{tiles}</ul>
</div>;
}
render() {
const Field = sdk.getComponent('elements.Field');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab mx_MjolnirUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Ignored users")}</div>
<div className="mx_SettingsTab_section">
<div className='mx_SettingsTab_subsectionText'>
<span className='warning'>{_t("⚠ These settings are meant for advanced users.")}</span><br />
<br />
<span className='warning'>{_t("⚠ These settings are meant for advanced users.")}</span><br/>
<br/>
{_t(
"Add users and servers you want to ignore here. Use asterisks " +
"to have Riot match any characters. For example, <code>@bot:*</code> " +
"would ignore all users that have the name 'bot' on any server.",
{}, {code: (s) => <code>{s}</code>},
)}<br />
<br />
)}<br/>
<br/>
{_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.",
)}
</div>
<p>TODO</p>
<div>
{this._renderPersonalBanListRules()}
</div>
<div>
<form onSubmit={this._onAddPersonalRule} autoComplete="off">
<Field
id="mx_MjolnirUserSettingsTab_personalAdd"
type="text"
label={_t("Server or user ID to ignore")}
placeholder={_t("eg: @bot:* or example.org")}
value={this.state.newPersonalRule}
onChange={this._onPersonalRuleChanged}
/>
<AccessibleButton type="submit" kind="primary" onClick={this._onAddPersonalRule}
disabled={this.state.busy}>
{_t("Ignore")}
</AccessibleButton>
</form>
</div>
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Subscribed lists")}</span>

View file

@ -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, <code>@bot:*</code> 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, <code>@bot:*</code> 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",

View file

@ -60,17 +60,23 @@ export class BanList {
return this._rules.filter(r => r.kind === RULE_ROOM);
}
banEntity(kind: string, entity: string, reason: string): Promise<any> {
return MatrixClientPeg.get().sendStateEvent(this._roomId, ruleTypeToStable(kind, true), {
async banEntity(kind: string, entity: string, reason: string): Promise<any> {
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<any> {
async unbanEntity(kind: string, entity: string): Promise<any> {
// 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);

View file

@ -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;

View file

@ -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