Skip to content

Commit deab4ad

Browse files
committed
mute: Request and consume new user_topic format in API
In this commit, we start requesting the new `user_topic` format for the data describing the "muted topics" feature. On new servers, that causes the new format to appear, and the old to disappear. Describe the API changes, and start consuming the new format. This should have no user-visible effect -- currently the new format describes exactly the same information as the old. Taking the client plus (a properly behaving) server as one system, this change is therefore NFC. We don't call the change NFC, though, because it does change the client's behavior as witnessed by the server. (This change does switch from stream names to stream IDs in the API... but only for data that we get from the server through the event system, not for data we send or receive in any request outside that system. That means that (unless affected by some server bug) it's always working with a stream name-to-ID mapping that exactly matches what the server had at the time it sent that data. So even if a stream gets renamed concurrently with getting some data here, there's no race and no bug. OTOH when we *change* this at the server on the user's behalf, that does suffer from such a race. But that part of the API was fixed long ago to accept stream IDs; we already have a TODO-server comment, at `api.setTopicMute`, to start relying on that update.) Fixes: #5380
1 parent d3dcc9e commit deab4ad

File tree

9 files changed

+235
-35
lines changed

9 files changed

+235
-35
lines changed

src/__tests__/lib/exampleData.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -737,9 +737,6 @@ export const action = Object.freeze({
737737
// InitialDataMessage
738738
max_message_id: 100,
739739

740-
// InitialDataMutedTopics
741-
muted_topics: [],
742-
743740
// InitialDataMutedUsers
744741
muted_users: [],
745742

@@ -950,6 +947,9 @@ export const action = Object.freeze({
950947

951948
// InitialDataUserStatus
952949
user_status: {},
950+
951+
// InitialDataUserTopic
952+
user_topics: [],
953953
},
954954
}): RegisterCompleteAction),
955955

src/actionTypes.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import type {
7575
RestartEvent,
7676
UpdateMessageEvent,
7777
RealmUserUpdateEvent,
78+
UserTopicEvent,
7879
} from './api/eventTypes';
7980
import type { MutedTopicTuple, PresenceSnapshot } from './api/apiTypes';
8081
import type { MessageMove } from './api/misc';
@@ -336,8 +337,9 @@ type GenericEventAction = $ReadOnly<{|
336337
| RestartEvent
337338
| RealmUpdateEvent
338339
| RealmUpdateDictEvent
340+
| RealmUserUpdateEvent
339341
| UserSettingsUpdateEvent
340-
| RealmUserUpdateEvent,
342+
| UserTopicEvent,
341343
|}>;
342344

343345
type EventNewMessageAction = $ReadOnly<{|

src/api/eventTypes.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {
2525
UserStatusUpdate,
2626
UserSettings,
2727
ClientPresence,
28+
UserTopic,
2829
} from './modelTypes';
2930
import type { RealmDataForUpdate } from './realmDataTypes';
3031

@@ -69,6 +70,7 @@ export const EventTypes = keyMirror({
6970
user_group: null,
7071
user_settings: null,
7172
user_status: null,
73+
user_topic: null,
7274
});
7375

7476
export type EventType = $Keys<typeof EventTypes>;
@@ -449,3 +451,11 @@ export type RealmUserUpdateEvent = {|
449451
// avatar_url_medium: string,
450452
|},
451453
|};
454+
455+
export type UserTopicEvent = {|
456+
...EventCommon,
457+
+type: typeof EventTypes.user_topic,
458+
459+
// The event comprises the whole new state of the user-topic relationship.
460+
...UserTopic,
461+
|};

src/api/initialDataTypes.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
UserId,
1818
UserSettings,
1919
UserStatusUpdate,
20+
UserTopic,
2021
} from './modelTypes';
2122
import type {
2223
CreatePublicOrPrivateStreamPolicyT,
@@ -90,7 +91,9 @@ export type InitialDataMessage = $ReadOnly<{|
9091
|}>;
9192

9293
export type InitialDataMutedTopics = $ReadOnly<{|
93-
muted_topics: $ReadOnlyArray<MutedTopicTuple>,
94+
/** Omitted by new servers that send `user_topics`; read that instead. */
95+
// TODO(server-6.0): Remove; gone in FL 134 (given that we request `user_topic`).
96+
muted_topics?: $ReadOnlyArray<MutedTopicTuple>,
9497
|}>;
9598

9699
export type InitialDataMutedUsers = $ReadOnly<{|
@@ -674,6 +677,18 @@ export type InitialDataUserStatus = $ReadOnly<{|
674677
|}>,
675678
|}>;
676679

680+
/** Initial data for the `user_topic` event type. */
681+
export type InitialDataUserTopic = {|
682+
/**
683+
* When absent (on older servers), read `muted_topics` instead.
684+
*
685+
* For user-topic pairs not found here, the value is implicitly a
686+
* zero value: `visibility_policy` is `UserTopicVisibilityPolicy.None`.
687+
*/
688+
// TODO(server-6.0): Introduced in FL 134.
689+
+user_topics?: $ReadOnlyArray<UserTopic>,
690+
|};
691+
677692
/**
678693
* The initial data snapshot sent on `/register`.
679694
*
@@ -718,6 +733,7 @@ export type InitialData = {|
718733
...InitialDataUpdateMessageFlags,
719734
...InitialDataUserSettings,
720735
...InitialDataUserStatus,
736+
...InitialDataUserTopic,
721737
|};
722738

723739
/**

src/api/registerForEvents.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export default async (
149149
'alert_words',
150150
'custom_profile_fields',
151151
'message',
152-
'muted_topics',
152+
'muted_topics', // TODO(server-6.0): Stop requesting, in favor of user_topic
153153
'muted_users',
154154
'presence',
155155
'realm',
@@ -170,6 +170,7 @@ export default async (
170170
'update_message_flags',
171171
'user_settings',
172172
'user_status',
173+
'user_topic',
173174
'zulip_version',
174175
]),
175176

src/events/eventToAction.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export default (state: PerAccountState, event: $FlowFixMe): EventAction | null =
168168
case 'custom_profile_fields':
169169
case 'stream':
170170
case 'user_settings':
171+
case 'user_topic':
171172
return {
172173
type: EVENT,
173174
event,

src/mute/__tests__/mute-testlib.js

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,27 @@
22

33
import { reducer } from '../muteModel';
44
import type { MuteState } from '../muteModelTypes';
5-
import type { Stream } from '../../api/modelTypes';
5+
import { type Stream, type UserTopic, UserTopicVisibilityPolicy } from '../../api/modelTypes';
66
import * as eg from '../../__tests__/lib/exampleData';
7-
import { EVENT_MUTED_TOPICS } from '../../actionConstants';
87

9-
export const makeMuteState = (data: $ReadOnlyArray<[Stream, string]>): MuteState => {
10-
const streams = new Set(data.map(([stream, topic]) => stream));
8+
export const makeUserTopic = (
9+
stream: Stream,
10+
topic: string,
11+
visibility_policy: UserTopicVisibilityPolicy,
12+
): UserTopic => ({
13+
stream_id: stream.stream_id,
14+
topic_name: topic,
15+
visibility_policy,
16+
last_updated: 12345, // arbitrary value; we ignore last_updated here
17+
});
1118

12-
return reducer(
19+
export const makeMuteState = (data: $ReadOnlyArray<[Stream, string]>): MuteState =>
20+
reducer(
1321
undefined,
14-
{
15-
type: EVENT_MUTED_TOPICS,
16-
id: -1,
17-
muted_topics: data.map(([stream, topic]) => [stream.name, topic]),
18-
},
19-
eg.reduxState({ streams: [...streams] }),
22+
eg.mkActionRegisterComplete({
23+
user_topics: data.map(([stream, topic]) =>
24+
makeUserTopic(stream, topic, UserTopicVisibilityPolicy.Muted),
25+
),
26+
}),
27+
eg.reduxState(),
2028
);
21-
};

src/mute/__tests__/muteModel-test.js

Lines changed: 120 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import deepFreeze from 'deep-freeze';
33

44
import fullReducer from '../../boot/reducers';
55
import { getMute, getTopicVisibilityPolicy, isTopicMuted, reducer } from '../muteModel';
6-
import { EVENT_MUTED_TOPICS } from '../../actionConstants';
6+
import { EVENT, EVENT_MUTED_TOPICS } from '../../actionConstants';
77
import * as eg from '../../__tests__/lib/exampleData';
8-
import { makeMuteState } from './mute-testlib';
8+
import { makeMuteState, makeUserTopic } from './mute-testlib';
99
import { tryGetActiveAccountState } from '../../selectors';
1010
import { UserTopicVisibilityPolicy } from '../../api/modelTypes';
11+
import { EventTypes } from '../../api/eventTypes';
1112

1213
/* eslint jest/expect-expect: ["error", { "assertFunctionNames": ["expect", "check"] }] */
1314

@@ -53,28 +54,55 @@ describe('getters', () => {
5354

5455
describe('reducer', () => {
5556
describe('REGISTER_COMPLETE', () => {
56-
test('when mute data is provided init state with it: local', () => {
57-
const action = eg.mkActionRegisterComplete({ muted_topics: [[eg.stream.name, 'topic']] });
57+
test('in modern user_topics format: unit test', () => {
58+
const action = eg.mkActionRegisterComplete({
59+
user_topics: [makeUserTopic(eg.stream, 'topic', UserTopicVisibilityPolicy.Muted)],
60+
});
5861
expect(reducer(initialState, action, eg.plusReduxState)).toEqual(
5962
makeMuteState([[eg.stream, 'topic']]),
6063
);
6164
});
6265

63-
test('when mute data is provided init state with it: end-to-end', () => {
66+
test('in modern user_topics format: end-to-end test', () => {
6467
const action = eg.mkActionRegisterComplete({
6568
streams: [eg.stream],
6669
subscriptions: [eg.subscription],
70+
user_topics: [makeUserTopic(eg.stream, 'topic', UserTopicVisibilityPolicy.Muted)],
71+
});
72+
const newState = tryGetActiveAccountState(fullReducer(eg.plusReduxState, action));
73+
expect(newState).toBeTruthy();
74+
expect(newState && getMute(newState)).toEqual(makeMuteState([[eg.stream, 'topic']]));
75+
});
76+
77+
test('in old muted_topics format: unit test', () => {
78+
const action = eg.mkActionRegisterComplete({
6779
muted_topics: [[eg.stream.name, 'topic']],
80+
user_topics: undefined,
81+
});
82+
expect(reducer(initialState, action, eg.plusReduxState)).toEqual(
83+
makeMuteState([[eg.stream, 'topic']]),
84+
);
85+
});
86+
87+
test('in old muted_topics format: end-to-end test', () => {
88+
const action = eg.mkActionRegisterComplete({
89+
streams: [eg.stream],
90+
subscriptions: [eg.subscription],
91+
muted_topics: [[eg.stream.name, 'topic']],
92+
user_topics: undefined,
6893
});
6994
const newState = tryGetActiveAccountState(fullReducer(eg.plusReduxState, action));
7095
expect(newState).toBeTruthy();
7196
expect(newState && getMute(newState)).toEqual(makeMuteState([[eg.stream, 'topic']]));
7297
});
7398

7499
// TODO(#5102): Delete; see comment on implementation.
75-
test('when no `mute` data is given reset state', () => {
100+
test('in ancient no-muted-topics format', () => {
76101
const state = makeMuteState([[eg.stream, 'topic']]);
77-
const action = eg.mkActionRegisterComplete({ muted_topics: [] });
102+
const action = eg.mkActionRegisterComplete({
103+
muted_topics: undefined,
104+
user_topics: undefined,
105+
});
78106
expect(reducer(state, action, eg.plusReduxState)).toEqual(initialState);
79107
});
80108
});
@@ -86,7 +114,91 @@ describe('reducer', () => {
86114
});
87115
});
88116

89-
describe('EVENT_MUTED_TOPICS', () => {
117+
describe('EVENT > user_topic', () => {
118+
function mkAction(userTopic) {
119+
return { type: EVENT, event: { id: 0, type: EventTypes.user_topic, ...userTopic } };
120+
}
121+
122+
function check(state, userTopic, expected) {
123+
expect(reducer(state, mkAction(userTopic), eg.plusReduxState)).toEqual(expected);
124+
}
125+
126+
test('add with new stream', () => {
127+
check(
128+
initialState,
129+
makeUserTopic(eg.stream, 'topic', UserTopicVisibilityPolicy.Muted),
130+
makeMuteState([[eg.stream, 'topic']]),
131+
);
132+
});
133+
134+
test('add in existing stream', () => {
135+
check(
136+
makeMuteState([[eg.stream, 'topic']]),
137+
makeUserTopic(eg.stream, 'other topic', UserTopicVisibilityPolicy.Muted),
138+
makeMuteState([
139+
[eg.stream, 'topic'],
140+
[eg.stream, 'other topic'],
141+
]),
142+
);
143+
});
144+
145+
test('remove, with others in stream', () => {
146+
check(
147+
makeMuteState([
148+
[eg.stream, 'topic'],
149+
[eg.stream, 'other topic'],
150+
]),
151+
makeUserTopic(eg.stream, 'other topic', UserTopicVisibilityPolicy.None),
152+
makeMuteState([[eg.stream, 'topic']]),
153+
);
154+
});
155+
156+
test('remove, as last in stream', () => {
157+
check(
158+
makeMuteState([[eg.stream, 'topic']]),
159+
makeUserTopic(eg.stream, 'topic', UserTopicVisibilityPolicy.None),
160+
initialState,
161+
);
162+
});
163+
164+
describe('redundantly after EVENT_MUTED_TOPICS', () => {
165+
// The server may send both muted_topics events and user_topic events,
166+
// because we don't set event_types in our /register request:
167+
// https://github.com/zulip/zulip/pull/21251#issuecomment-1133466148
168+
// So we may get one of these after a muted_topics event has already
169+
// set the new state.
170+
//
171+
// (Or we might get user_topic first and then muted_topics, but that
172+
// doesn't require any testing of its own -- when handling the
173+
// muted_topics event it doesn't matter what the previous state was.)
174+
175+
test('add', () => {
176+
check(
177+
makeMuteState([[eg.stream, 'topic']]),
178+
makeUserTopic(eg.stream, 'topic', UserTopicVisibilityPolicy.Muted),
179+
makeMuteState([[eg.stream, 'topic']]),
180+
);
181+
});
182+
183+
test('remove, leaving others in stream', () => {
184+
check(
185+
makeMuteState([[eg.stream, 'topic']]),
186+
makeUserTopic(eg.stream, 'other topic', UserTopicVisibilityPolicy.None),
187+
makeMuteState([[eg.stream, 'topic']]),
188+
);
189+
});
190+
191+
test('remove, as last in stream', () => {
192+
check(
193+
makeMuteState([]),
194+
makeUserTopic(eg.stream, 'topic', UserTopicVisibilityPolicy.None),
195+
makeMuteState([]),
196+
);
197+
});
198+
});
199+
});
200+
201+
describe('EVENT_MUTED_TOPICS (legacy)', () => {
90202
test('appends and test a new muted topic', () => {
91203
const action = deepFreeze({
92204
type: EVENT_MUTED_TOPICS,

0 commit comments

Comments
 (0)