Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Add ignore user API support #1389

Merged
merged 12 commits into from
Sep 17, 2017
53 changes: 53 additions & 0 deletions src/SlashCommands.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,59 @@ const commands = {
return reject(this.getUsage());
}),

ignore: new Command("ignore", "<userId>", function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+)$/);
if (matches) {
const userId = matches[1];
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
ignoredUsers.push(userId); // de-duped internally in the js-sdk
return success(
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, {
title: _t("Ignored user"),
description: (
<div>
<p>{_t("You are now ignoring %(userId)s", {userId: userId})}</p>
</div>
),
hasCancelButton: false,
});
}),
);
}
}
return reject(this.getUsage());
}),

unignore: new Command("unignore", "<userId>", function(roomId, args) {
if (args) {
const matches = args.match(/^(\S+)$/);
if (matches) {
const userId = matches[1];
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
const index = ignoredUsers.indexOf(userId);
if (index !== -1) ignoredUsers.splice(index, 1);
return success(
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, {
title: _t("Unignored user"),
description: (
<div>
<p>{_t("You are no longer ignoring %(userId)s", {userId: userId})}</p>
</div>
),
hasCancelButton: false,
});
}),
);
}
}
return reject(this.getUsage());
}),

// Define the power level of a user
op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
if (args) {
Expand Down
6 changes: 6 additions & 0 deletions src/WhoIsTyping.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ var MatrixClientPeg = require("./MatrixClientPeg");
import { _t } from './languageHandler';

module.exports = {
usersTypingApartFromMeAndIgnored: function(room) {
return this.usersTyping(
room, [MatrixClientPeg.get().credentials.userId].concat(MatrixClientPeg.get().getIgnoredUsers())
);
},

usersTypingApartFromMe: function(room) {
return this.usersTyping(
room, [MatrixClientPeg.get().credentials.userId]
Expand Down
10 changes: 10 additions & 0 deletions src/autocomplete/CommandProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,16 @@ const COMMANDS = [
args: '<user-id> <device-id> <device-signing-key>',
description: 'Verifies a user, device, and pubkey tuple',
},
{
command: '/ignore',
args: '<user-id>',
description: 'Ignores a user, hiding their messages from you',
},
{
command: '/unignore',
args: '<user-id>',
description: 'Stops ignoring a user, showing their messages going forward',
},
// Omitting `/markdown` as it only seems to apply to OldComposer
];

Expand Down
3 changes: 3 additions & 0 deletions src/components/structures/LoggedInView.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ export default React.createClass({
useCompactLayout: event.getContent().useCompactLayout,
});
}
if (event.getType() === "m.ignored_user_list") {
dis.dispatch({action: "ignore_state_changed"});
}
},

_onKeyDown: function(ev) {
Expand Down
7 changes: 7 additions & 0 deletions src/components/structures/MessagePanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ module.exports = React.createClass({

// TODO: Implement granular (per-room) hide options
_shouldShowEvent: function(mxEv) {
if (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) {
return false; // ignored = no show (only happens if the ignore happens after an event was received)
}

const EventTile = sdk.getComponent('rooms.EventTile');
if (!EventTile.haveTileForEvent(mxEv)) {
return false; // no tile = no show
Expand Down Expand Up @@ -549,6 +553,9 @@ module.exports = React.createClass({
if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
return; // ignore non-read receipts and receipts from self.
}
if (MatrixClientPeg.get().isUserIgnored(r.userId)) {
return; // ignore ignored users
}
let member = room.getMember(r.userId);
if (!member) {
return; // ignore unknown user IDs
Expand Down
2 changes: 1 addition & 1 deletion src/components/structures/RoomStatusBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ module.exports = React.createClass({

onRoomMemberTyping: function(ev, member) {
this.setState({
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
usersTyping: WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room),
});
},

Expand Down
3 changes: 3 additions & 0 deletions src/components/structures/TimelinePanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,9 @@ var TimelinePanel = React.createClass({
this.sendReadReceipt();
this.updateReadMarker();
break;
case 'ignore_state_changed':
this.forceUpdate();
break;
}
},

Expand Down
64 changes: 64 additions & 0 deletions src/components/structures/UserSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,34 @@ const THEMES = [
},
];

const IgnoredUser = React.createClass({
propTypes: {
userId: React.PropTypes.string.isRequired,
onUnignored: React.PropTypes.func.isRequired,
},

_onUnignoreClick: function() {
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
const index = ignoredUsers.indexOf(this.props.userId);
if (index !== -1) {
ignoredUsers.splice(index, 1);
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers)
.then(() => this.props.onUnignored(this.props.userId));
} else this.props.onUnignored(this.props.userId);
},

render: function() {
return (
<li>
<AccessibleButton onClick={this._onUnignoreClick} className="mx_UserSettings_button mx_UserSettings_buttonSmall">
{ _t("Unignore") }
</AccessibleButton>
{ this.props.userId }
</li>
);
},
});

module.exports = React.createClass({
displayName: 'UserSettings',

Expand Down Expand Up @@ -207,6 +235,7 @@ module.exports = React.createClass({
vectorVersion: undefined,
rejectingInvites: false,
mediaDevices: null,
ignoredUsers: [],
};
},

Expand All @@ -228,6 +257,7 @@ module.exports = React.createClass({
}

this._refreshMediaDevices();
this._refreshIgnoredUsers();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess technically we might want to also _refreshIgnoreUsers() when we receive an ignoring event from the js-sdk. But that's such an edge case i'm happy to ignore it for now :)

Copy link
Member

@ara4n ara4n Sep 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(although given we already have an onAction() here, surely it's just 3 lines to add in a:

case 'ignore_state_changed':
                this._refreshIgnoredUsers();
                break;

...which almost seems worth it.)


// Bulk rejecting invites:
// /sync won't have had time to return when UserSettings re-renders from state changes, so getRooms()
Expand Down Expand Up @@ -346,9 +376,22 @@ module.exports = React.createClass({
});
},

_refreshIgnoredUsers: function(userIdUnignored=null) {
const users = MatrixClientPeg.get().getIgnoredUsers();
if (userIdUnignored) {
const index = users.indexOf(userIdUnignored);
if (index !== -1) users.splice(index, 1);
}
this.setState({
ignoredUsers: users,
});
},

onAction: function(payload) {
if (payload.action === "notifier_enabled") {
this.forceUpdate();
} else if (payload.action === "ignore_state_changed") {
this._refreshIgnoredUsers();
}
},

Expand Down Expand Up @@ -796,6 +839,26 @@ module.exports = React.createClass({
);
},

_renderIgnoredUsers: function() {
if (this.state.ignoredUsers.length > 0) {
const updateHandler = this._refreshIgnoredUsers;
return (
<div>
<h3>{ _t("Ignored Users") }</h3>
<div className="mx_UserSettings_section mx_UserSettings_ignoredUsersSection">
<ul>
{this.state.ignoredUsers.map(function(userId) {
return (<IgnoredUser key={userId}
userId={userId}
onUnignored={updateHandler}></IgnoredUser>);
})}
</ul>
</div>
</div>
);
} else return (<div />);
},

_renderLocalSetting: function(setting) {
// TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render
Expand Down Expand Up @@ -1302,6 +1365,7 @@ module.exports = React.createClass({
{this._renderWebRtcSettings()}
{this._renderDevicesPanel()}
{this._renderCryptoInfo()}
{this._renderIgnoredUsers()}
{this._renderBulkOptions()}
{this._renderBugReport()}

Expand Down
45 changes: 45 additions & 0 deletions src/components/views/rooms/MemberInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ module.exports = withMatrixClient(React.createClass({
updating: 0,
devicesLoading: true,
devices: null,
isIgnoring: false,
};
},

Expand All @@ -81,6 +82,8 @@ module.exports = withMatrixClient(React.createClass({
cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("accountData", this.onAccountData);

this._checkIgnoreState();
},

componentDidMount: function() {
Expand Down Expand Up @@ -111,6 +114,11 @@ module.exports = withMatrixClient(React.createClass({
}
},

_checkIgnoreState: function() {
const isIgnoring = this.props.matrixClient.isUserIgnored(this.props.member.userId);
this.setState({isIgnoring: isIgnoring});
},

_disambiguateDevices: function(devices) {
var names = Object.create(null);
for (var i = 0; i < devices.length; i++) {
Expand Down Expand Up @@ -225,6 +233,18 @@ module.exports = withMatrixClient(React.createClass({
});
},

onIgnoreToggle: function() {
const ignoredUsers = this.props.matrixClient.getIgnoredUsers();
if (this.state.isIgnoring) {
const index = ignoredUsers.indexOf(this.props.member.userId);
if (index !== -1) ignoredUsers.splice(index, 1);
} else {
ignoredUsers.push(this.props.member.userId);
}

this.props.matrixClient.setIgnoredUsers(ignoredUsers).then(() => this.setState({isIgnoring: !this.state.isIgnoring}));
},

onKick: function() {
const membership = this.props.member.membership;
const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick");
Expand Down Expand Up @@ -607,6 +627,29 @@ module.exports = withMatrixClient(React.createClass({
);
},

_renderUserOptions: function() {
// Only allow the user to ignore the user if its not ourselves
let ignoreButton = null;
if (this.props.member.userId !== this.props.matrixClient.getUserId()) {
ignoreButton = (
<AccessibleButton onClick={this.onIgnoreToggle} className="mx_MemberInfo_field">
{this.state.isIgnoring ? _t("Unignore") : _t("Ignore")}
</AccessibleButton>
);
}

if (!ignoreButton) return null;

return (
<div>
<h3>{ _t("User Options") }</h3>
<div className="mx_MemberInfo_buttons">
{ignoreButton}
</div>
</div>
);
},

render: function() {
var startChat, kickButton, banButton, muteButton, giveModButton, spinner;
if (this.props.member.userId !== this.props.matrixClient.credentials.userId) {
Expand Down Expand Up @@ -756,6 +799,8 @@ module.exports = withMatrixClient(React.createClass({
</div>
</div>

{ this._renderUserOptions() }

{ adminTools }

{ startChat }
Expand Down
10 changes: 10 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,16 @@
"Kick": "Kick",
"Kicks user with given id": "Kicks user with given id",
"Labs": "Labs",
"Ignored Users": "Ignored Users",
"Ignore": "Ignore",
"Unignore": "Unignore",
"User Options": "User Options",
"You are now ignoring %(userId)s": "You are now ignoring %(userId)s",
"You are no longer ignoring %(userId)s": "You are no longer ignoring %(userId)s",
"Unignored user": "Unignored user",
"Ignored user": "Ignored user",
"Stops ignoring a user, showing their messages going forward": "Stops ignoring a user, showing their messages going forward",
"Ignores a user, hiding their messages from you": "Ignores a user, hiding their messages from you",
"Last seen": "Last seen",
"Leave room": "Leave room",
"left and rejoined": "left and rejoined",
Expand Down
10 changes: 10 additions & 0 deletions src/i18n/strings/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,16 @@
"Kick": "Kick",
"Kicks user with given id": "Kicks user with given id",
"Labs": "Labs",
"Ignored Users": "Ignored Users",
"Ignore": "Ignore",
"Unignore": "Unignore",
"User Options": "User Options",
"You are now ignoring %(userId)s": "You are now ignoring %(userId)s",
"You are no longer ignoring %(userId)s": "You are no longer ignoring %(userId)s",
"Unignored user": "Unignored user",
"Ignored user": "Ignored user",
"Stops ignoring a user, showing their messages going forward": "Stops ignoring a user, showing their messages going forward",
"Ignores a user, hiding their messages from you": "Ignores a user, hiding their messages from you",
"Leave room": "Leave room",
"left and rejoined": "left and rejoined",
"left": "left",
Expand Down
6 changes: 5 additions & 1 deletion test/components/structures/MessagePanel-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var sdk = require('matrix-react-sdk');

var MessagePanel = sdk.getComponent('structures.MessagePanel');
import UserSettingsStore from '../../../src/UserSettingsStore';
import MatrixClientPeg from '../../../src/MatrixClientPeg';

var test_utils = require('test-utils');
var mockclock = require('mock-clock');
Expand Down Expand Up @@ -51,16 +52,19 @@ describe('MessagePanel', function () {
var clock = mockclock.clock();
var realSetTimeout = window.setTimeout;
var events = mkEvents();
var sandbox = null;

beforeEach(function() {
test_utils.beforeEach(this);
client = test_utils.createTestClient();
sandbox = test_utils.stubClient();
client = MatrixClientPeg.get();
client.credentials = {userId: '@me:here'};
UserSettingsStore.getSyncedSettings = sinon.stub().returns({});
});

afterEach(function() {
clock.uninstall();
sandbox.restore();
});

function mkEvents() {
Expand Down