Skip to content

Commit 1eca694

Browse files
committed
mute: Enrich model to support new user_topic API
Ideally we'd rename this to something like `state.userTopic` too. But it's currently a pain to actually successfully rename a state subtree (and not have its ghost return at the old name): #5381. So just keep calling it "mute" for now. Also add tests for the model. Note that we don't have to update any of the existing tests! They use `makeMuteState`, which uses the reducer; so they get the new data structure automatically.
1 parent 75c533c commit 1eca694

File tree

5 files changed

+102
-13
lines changed

5 files changed

+102
-13
lines changed

src/mute/__tests__/muteModel-test.js

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,55 @@
22
import deepFreeze from 'deep-freeze';
33

44
import fullReducer from '../../boot/reducers';
5-
import { getMute, reducer } from '../muteModel';
5+
import { getMute, getTopicVisibilityPolicy, isTopicMuted, reducer } from '../muteModel';
66
import { EVENT_MUTED_TOPICS } from '../../actionConstants';
77
import * as eg from '../../__tests__/lib/exampleData';
88
import { makeMuteState } from './mute-testlib';
99
import { tryGetActiveAccountState } from '../../selectors';
10+
import { UserTopicVisibilityPolicy } from '../../api/modelTypes';
11+
12+
/* eslint jest/expect-expect: ["error", { "assertFunctionNames": ["expect", "check"] }] */
1013

1114
const initialState = makeMuteState([]);
1215

16+
describe('getters', () => {
17+
describe('getTopicVisibilityPolicy', () => {
18+
function check(state, expected) {
19+
expect(getTopicVisibilityPolicy(state, eg.stream.stream_id, 'topic')).toEqual(expected);
20+
}
21+
22+
test('with nothing for stream', () => {
23+
check(makeMuteState([]), UserTopicVisibilityPolicy.None);
24+
});
25+
26+
test('with nothing for topic', () => {
27+
check(makeMuteState([[eg.stream, 'other topic']]), UserTopicVisibilityPolicy.None);
28+
});
29+
30+
test('with topic muted', () => {
31+
check(makeMuteState([[eg.stream, 'topic']]), UserTopicVisibilityPolicy.Muted);
32+
});
33+
});
34+
35+
describe('isTopicMuted', () => {
36+
function check(state, expected) {
37+
expect(isTopicMuted(eg.stream.stream_id, 'topic', state)).toEqual(expected);
38+
}
39+
40+
test('with nothing for stream', () => {
41+
check(makeMuteState([]), false);
42+
});
43+
44+
test('with nothing for topic', () => {
45+
check(makeMuteState([[eg.stream, 'other topic']]), false);
46+
});
47+
48+
test('with topic muted', () => {
49+
check(makeMuteState([[eg.stream, 'topic']]), true);
50+
});
51+
});
52+
});
53+
1354
describe('reducer', () => {
1455
describe('REGISTER_COMPLETE', () => {
1556
test('when mute data is provided init state with it: local', () => {

src/mute/muteModel.js

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/* @flow strict-local */
2+
import Immutable from 'immutable';
23

34
import type { MuteState, PerAccountApplicableAction, PerAccountState } from '../types';
5+
import { UserTopicVisibilityPolicy } from '../api/modelTypes';
46
import { REGISTER_COMPLETE, EVENT_MUTED_TOPICS, RESET_ACCOUNT_DATA } from '../actionConstants';
57
import { getStreamsByName } from '../subscriptions/subscriptionSelectors';
68
import * as logging from '../utils/logging';
@@ -18,27 +20,52 @@ export const getMute = (state: PerAccountState): MuteState => state.mute;
1820
// Getters.
1921
//
2022

21-
export const isTopicMuted = (streamId: number, topic: string, mute: MuteState): boolean =>
22-
mute.get(streamId)?.has(topic) ?? false;
23+
export function getTopicVisibilityPolicy(
24+
mute: MuteState,
25+
streamId: number,
26+
topic: string,
27+
): UserTopicVisibilityPolicy {
28+
return mute.get(streamId)?.get(topic) ?? UserTopicVisibilityPolicy.None;
29+
}
30+
31+
export function isTopicMuted(streamId: number, topic: string, mute: MuteState): boolean {
32+
const policy = getTopicVisibilityPolicy(mute, streamId, topic);
33+
switch (policy) {
34+
case UserTopicVisibilityPolicy.None:
35+
return false;
36+
case UserTopicVisibilityPolicy.Muted:
37+
return true;
38+
}
39+
}
2340

2441
//
2542
//
2643
// Reducer.
2744
//
2845

29-
const initialState: MuteState = new Map();
46+
const initialState: MuteState = Immutable.Map();
3047

3148
function convert(data, streams): MuteState {
32-
const result = new DefaultMap(() => new Set());
49+
// Turn the incoming array into a nice, indexed, Immutable data structure
50+
// in the same two-phase pattern we use for the unread data, as an
51+
// optimization. Here it's probably much less often enough data for the
52+
// optimization to matter; but a longtime user who regularly uses this
53+
// feature will accumulate many records over time, and then it could.
54+
55+
// First, collect together all the data for a given stream, just in a
56+
// plain old Array.
57+
const byStream = new DefaultMap(() => []);
3358
for (const [streamName, topic] of data) {
3459
const stream = streams.get(streamName);
3560
if (!stream) {
3661
logging.warn('mute: unknown stream');
3762
continue;
3863
}
39-
result.getOrCreate(stream.stream_id).add(topic);
64+
byStream.getOrCreate(stream.stream_id).push([topic, UserTopicVisibilityPolicy.Muted]);
4065
}
41-
return result.map;
66+
67+
// Then, from each of those build an Immutable.Map all in one shot.
68+
return Immutable.Map(Immutable.Seq.Keyed(byStream.map.entries()).map(Immutable.Map));
4269
}
4370

4471
export const reducer = (

src/mute/muteModelTypes.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
// @flow strict-local
2+
import Immutable from 'immutable';
23

3-
// No need for Immutable, because -- the server API actually doesn't support
4-
// incremental changes! When something changes, it sends the entire new list.
5-
export type MuteState = Map<number, Set<string>>; // stream ID, topic
4+
import { type UserTopicVisibilityPolicy } from '../api/modelTypes';
5+
6+
/**
7+
* The "visibility policy" our user has chosen for each topic.
8+
*
9+
* See jsdoc of UserTopicVisibilityPolicy for background.
10+
*
11+
* In this data structure, the keys are stream ID and then topic name.
12+
* Values of `UserTopicVisibilityPolicy.None` are represented by absence,
13+
* and streams where the map would be empty are also omitted.
14+
*/
15+
// TODO(#5381): Ideally we'd call this UserTopicState and `state.userTopic`.
16+
// But it's currently a pain to actually rename a state subtree: #5381.
17+
export type MuteState = Immutable.Map<
18+
number, // stream ID
19+
Immutable.Map<
20+
string, // topic name
21+
UserTopicVisibilityPolicy,
22+
>,
23+
>;

src/storage/__tests__/migrations-test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ describe('migrations', () => {
105105
// What `base` becomes after all migrations.
106106
const endBase = {
107107
...base52,
108-
migrations: { version: 58 },
108+
migrations: { version: 59 },
109109
};
110110

111111
for (const [desc, before, after] of [
@@ -128,9 +128,9 @@ describe('migrations', () => {
128128
// redundant with this one, because none of the migration steps notice
129129
// whether any properties outside `storeKeys` are present or not.
130130
[
131-
'check dropCache at 56',
131+
'check dropCache at 59',
132132
// Just before the `dropCache`, plus a `cacheKeys` property, plus junk.
133-
{ ...base52, migrations: { version: 55 }, mute: [], nonsense: [1, 2, 3] },
133+
{ ...base52, migrations: { version: 58 }, mute: [], nonsense: [1, 2, 3] },
134134
// Should wind up with the same result as without the extra properties.
135135
endBase,
136136
],

src/storage/migrations.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,9 @@ const migrationsInner: {| [string]: (LessPartialState) => LessPartialState |} =
502502
),
503503
}),
504504

505+
// Change `state.mute` data structure: was a plain JS Map.
506+
'59': dropCache,
507+
505508
// TIP: When adding a migration, consider just using `dropCache`.
506509
// (See its jsdoc for guidance on when that's the right answer.)
507510
};

0 commit comments

Comments
 (0)