Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions src/__tests__/lib/exampleData.js
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@ export const action = deepFreeze({
alert_words: [],
max_message_id: 100,
muted_topics: [],
muted_users: [],
presences: {},
max_icon_file_size: 3,
realm_add_emoji_by_admins_only: true,
Expand Down Expand Up @@ -671,6 +672,8 @@ export const action = deepFreeze({
foundOldest: undefined,
ownUserId: selfUser.user_id,
},
// If a given action is only relevant to a single test file, no need to
// provide a generic example of it here; just define it there.
});

// Ensure every `eg.action.foo` is some well-typed action. (We don't simply
Expand All @@ -682,13 +685,27 @@ export const action = deepFreeze({
/* ========================================================================
* Action fragments
*
* Partial actions, for those action types whose interior will almost always
* need to be filled in with more data.
* Partial actions, for those action types where (a) there's some
* boilerplate data that's useful to supply here, but (b) there's some other
* places where a given test will almost always need to fill in specific
* data of its own.
*
* The properties where each test will want to fill in its own specific data
* should be left out of these fragments. That way, Flow can ensure the
* test explicitly supplies the data.
*/

export const eventNewMessageActionBase /* \: $Diff<EventNewMessageAction, {| message: Message |}> */ = {
type: EVENT_NEW_MESSAGE,
// These properties are boring for most or all tests.
id: 1001,
caughtUp: {},
ownUserId: selfUser.user_id,

// The details of this property are typically important to what a test is
// testing, so we provide it explicitly in each test.
// message: Message,
};

// If a given action is only relevant to a single test file, no need to
// provide a generic fragment for it here; just define the test data there.
1 change: 1 addition & 0 deletions src/actionConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const EVENT_USER_ADD: 'EVENT_USER_ADD' = 'EVENT_USER_ADD';
export const EVENT_USER_REMOVE: 'EVENT_USER_REMOVE' = 'EVENT_USER_REMOVE';
export const EVENT_USER_UPDATE: 'EVENT_USER_UPDATE' = 'EVENT_USER_UPDATE';
export const EVENT_MUTED_TOPICS: 'EVENT_MUTED_TOPICS' = 'EVENT_MUTED_TOPICS';
export const EVENT_MUTED_USERS: 'EVENT_MUTED_USERS' = 'EVENT_MUTED_USERS';

export const EVENT_USER_GROUP_ADD: 'EVENT_USER_GROUP_ADD' = 'EVENT_USER_GROUP_ADD';
export const EVENT_USER_GROUP_REMOVE: 'EVENT_USER_GROUP_REMOVE' = 'EVENT_USER_GROUP_REMOVE';
Expand Down
15 changes: 14 additions & 1 deletion src/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
EVENT_ALERT_WORDS,
INIT_TOPICS,
EVENT_MUTED_TOPICS,
EVENT_MUTED_USERS,
EVENT_REALM_FILTERS,
EVENT_USER_REMOVE,
EVENT_USER_UPDATE,
Expand All @@ -56,7 +57,13 @@ import {
EVENT,
} from './actionConstants';

import type { MessageEvent, PresenceEvent, StreamEvent, SubmessageEvent } from './api/eventTypes';
import type {
MessageEvent,
MutedUsersEvent,
PresenceEvent,
StreamEvent,
SubmessageEvent,
} from './api/eventTypes';

import type {
Orientation,
Expand Down Expand Up @@ -428,6 +435,11 @@ type EventMutedTopicsAction = {|
muted_topics: MuteState,
|};

type EventMutedUsersAction = {|
...MutedUsersEvent,
type: typeof EVENT_MUTED_USERS,
|};

type EventUserGroupAddAction = {|
...ServerEvent,
type: typeof EVENT_USER_GROUP_ADD,
Expand Down Expand Up @@ -512,6 +524,7 @@ export type EventAction =
| EventAlertWordsAction
| EventMessageDeleteAction
| EventMutedTopicsAction
| EventMutedUsersAction
| EventNewMessageAction
| EventSubmessageAction
| EventPresenceAction
Expand Down
9 changes: 8 additions & 1 deletion src/api/eventTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
* @flow strict-local
*/

import type { Message, Stream, UserId, UserPresence } from './modelTypes';
import type { Message, MutedUser, Stream, UserId, UserPresence } from './modelTypes';

export class EventTypes {
static alert_words: 'alert_words' = 'alert_words';
static delete_message: 'delete_message' = 'delete_message';
static heartbeat: 'heartbeat' = 'heartbeat';
static message: 'message' = 'message';
static muted_topics: 'muted_topics' = 'muted_topics';
static muted_users: 'muted_users' = 'muted_users';
static presence: 'presence' = 'presence';
static reaction: 'reaction' = 'reaction';
static realm_bot: 'realm_bot' = 'realm_bot';
Expand Down Expand Up @@ -70,6 +71,12 @@ export type MessageEvent = {|
local_message_id?: number,
|};

export type MutedUsersEvent = {|
...EventCommon,
type: typeof EventTypes.muted_users,
muted_users: MutedUser[],
|};

/** A new submessage. See the `Submessage` type for details. */
export type SubmessageEvent = {|
...EventCommon,
Expand Down
7 changes: 7 additions & 0 deletions src/api/initialDataTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import type {
CrossRealmBot,
MutedUser,
RealmEmojiById,
RealmFilter,
RecentPrivateConversation,
Expand Down Expand Up @@ -34,6 +35,11 @@ export type InitialDataMutedTopics = {|
muted_topics: MuteTuple[],
|};

/** Added in server version 4.0, feature level 48 */
export type InitialDataMutedUsers = {|
muted_users?: MutedUser[],
|};

export type InitialDataPresence = {|
presences: {| [email: string]: UserPresence |},
|};
Expand Down Expand Up @@ -311,6 +317,7 @@ export type InitialData = {|
...InitialDataAlertWords,
...InitialDataMessage,
...InitialDataMutedTopics,
...InitialDataMutedUsers,
...InitialDataPresence,
...InitialDataRealm,
...InitialDataRealmEmoji,
Expand Down
8 changes: 8 additions & 0 deletions src/api/modelTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,14 @@ export type UserPresence = {|
[client: string]: ClientPresence,
|};

/** This is what appears in the `muted_users` server event.
* See https://chat.zulip.org/api/get-events#muted_users for details.
*/
export type MutedUser = {|
id: UserId,
timestamp: number,
|};

//
//
//
Expand Down
2 changes: 2 additions & 0 deletions src/boot/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import flags from '../chat/flagsReducer';
import narrows from '../chat/narrowsReducer';
import messages from '../message/messagesReducer';
import mute from '../mute/muteReducer';
import mutedUsers from '../mute/mutedUsersReducer';
import outbox from '../outbox/outboxReducer';
import { reducer as pmConversations } from '../pm-conversations/pmConversationsModel';
import presence from '../presence/presenceReducer';
Expand Down Expand Up @@ -89,6 +90,7 @@ export default (state: void | GlobalState, action: Action): GlobalState => {
messages: applyReducer('messages', messages, state?.messages, action, state),
narrows: applyReducer('narrows', narrows, state?.narrows, action, state),
mute: applyReducer('mute', mute, state?.mute, action, state),
mutedUsers: applyReducer('mutedUsers', mutedUsers, state?.mutedUsers, action, state),
outbox: applyReducer('outbox', outbox, state?.outbox, action, state),
pmConversations: applyReducer('pmConversations', pmConversations, state?.pmConversations, action, state),
presence: applyReducer('presence', presence, state?.presence, action, state),
Expand Down
4 changes: 2 additions & 2 deletions src/boot/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ export const storeKeys: Array<$Keys<GlobalState>> = [
*/
// prettier-ignore
export const cacheKeys: Array<$Keys<GlobalState>> = [
'flags', 'messages', 'mute', 'narrows', 'pmConversations', 'realm', 'streams',
'subscriptions', 'unread', 'userGroups', 'users',
'flags', 'messages', 'mute', 'mutedUsers', 'narrows', 'pmConversations',
'realm', 'streams', 'subscriptions', 'unread', 'userGroups', 'users',
];

/**
Expand Down
1 change: 1 addition & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const config: Config = {
'alert_words',
'message',
'muted_topics',
'muted_users',
Comment thread
gnprice marked this conversation as resolved.
'presence',
'realm',
'realm_emoji',
Expand Down
3 changes: 3 additions & 0 deletions src/directSelectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
FlagsState,
MessagesState,
MuteState,
MutedUsersState,
NarrowsState,
TopicsState,
PresenceState,
Expand Down Expand Up @@ -44,6 +45,8 @@ export const getMessages = (state: GlobalState): MessagesState => state.messages

export const getMute = (state: GlobalState): MuteState => state.mute;

export const getMutedUsers = (state: GlobalState): MutedUsersState => state.mutedUsers;

export const getTyping = (state: GlobalState): TypingState => state.typing;

export const getTopics = (state: GlobalState): TopicsState => state.topics;
Expand Down
3 changes: 3 additions & 0 deletions src/events/eventToAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
EVENT_USER_REMOVE,
EVENT_USER_UPDATE,
EVENT_MUTED_TOPICS,
EVENT_MUTED_USERS,
EVENT_USER_GROUP_ADD,
EVENT_USER_GROUP_REMOVE,
EVENT_USER_GROUP_UPDATE,
Expand Down Expand Up @@ -59,6 +60,7 @@ const actionTypeOfEventType = {
subscription: EVENT_SUBSCRIPTION,
presence: EVENT_PRESENCE,
muted_topics: EVENT_MUTED_TOPICS,
muted_users: EVENT_MUTED_USERS,
realm_emoji: EVENT_REALM_EMOJI_UPDATE,
realm_filters: EVENT_REALM_FILTERS,
submessage: EVENT_SUBMESSAGE,
Expand Down Expand Up @@ -143,6 +145,7 @@ export default (state: GlobalState, event: $FlowFixMe): EventAction | null => {
case 'subscription':
case 'presence':
case 'muted_topics':
case 'muted_users':
case 'realm_emoji':
case 'realm_filters':
case 'submessage':
Expand Down
55 changes: 55 additions & 0 deletions src/mute/__tests__/mutedUsersReducer-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* @flow strict-local */
import Immutable from 'immutable';
Comment thread
gnprice marked this conversation as resolved.
import deepFreeze from 'deep-freeze';

import mutedUsersReducer from '../mutedUsersReducer';
import { EVENT_MUTED_USERS } from '../../actionConstants';
import * as eg from '../../__tests__/lib/exampleData';

describe('mutedUsersReducer', () => {
const baseState = Immutable.Map([[eg.otherUser.user_id, 1618822632]]);

describe('REALM_INIT', () => {
test('when `muted_users` data is provided init state with it', () => {
const action = deepFreeze({
...eg.action.realm_init,
data: {
...eg.action.realm_init.data,
muted_users: [{ id: eg.otherUser.user_id, timestamp: 1618822632 }],
},
});
expect(mutedUsersReducer(eg.baseReduxState.mutedUsers, action)).toEqual(
Immutable.Map([[eg.otherUser.user_id, 1618822632]]),
);
});

test('when no `muted_users` data is given reset state', () => {
expect(mutedUsersReducer(baseState, eg.action.realm_init)).toEqual(Immutable.Map());
});
});

describe('ACCOUNT_SWITCH', () => {
test('resets state to initial state', () => {
expect(mutedUsersReducer(baseState, eg.action.account_switch)).toEqual(Immutable.Map());
});
});

describe('EVENT_MUTED_USERS', () => {
test('update `muted_users` when event comes in', () => {
const action = deepFreeze({
type: EVENT_MUTED_USERS,
id: 1234,
muted_users: [
{ id: eg.otherUser.user_id, timestamp: 1618822632 },
{ id: eg.thirdUser.user_id, timestamp: 1618822635 },
],
});

// prettier-ignore
expect(mutedUsersReducer(baseState, action)).toEqual(Immutable.Map([
[eg.otherUser.user_id, 1618822632],
[eg.thirdUser.user_id, 1618822635],
]));
});
});
});
36 changes: 36 additions & 0 deletions src/mute/mutedUsersReducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* @flow strict-local */
import Immutable from 'immutable';

import type { MutedUsersState, Action, UserId } from '../types';
import {
REALM_INIT,
LOGIN_SUCCESS,
LOGOUT,
ACCOUNT_SWITCH,
EVENT_MUTED_USERS,
} from '../actionConstants';
import type { MutedUser } from '../api/apiTypes';

const initialState: MutedUsersState = Immutable.Map();

function mutedUsersToMap(muted_users: MutedUser[]): Immutable.Map<UserId, number> {
return Immutable.Map(muted_users.map(muted_user => [muted_user.id, muted_user.timestamp]));
}

export default (state: MutedUsersState = initialState, action: Action): MutedUsersState => {
switch (action.type) {
case LOGOUT:
case ACCOUNT_SWITCH:
case LOGIN_SUCCESS:
return initialState;

case REALM_INIT:
return mutedUsersToMap(action.data.muted_users ?? []);

case EVENT_MUTED_USERS:
return mutedUsersToMap(action.muted_users);

default:
return state;
}
};
4 changes: 4 additions & 0 deletions src/reduxTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ export type MigrationsState = {|

export type MuteState = MuteTuple[];

/** A map from user IDs to the Unix timestamp in seconds when they were muted. */
export type MutedUsersState = Immutable.Map<UserId, number>;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm glad to have another case of Immutable.Maps with numeric keys! 🎉 b54d68d should ensure that these get "revived" from storage with their keys as numbers, instead of strings. The data is persisted as JSON so we've had to be careful about that.


/**
* An index on `MessagesState`, listing messages in each narrow.
*
Expand Down Expand Up @@ -320,6 +323,7 @@ export type GlobalState = {|
messages: MessagesState,
migrations: MigrationsState,
mute: MuteState,
mutedUsers: MutedUsersState,
narrows: NarrowsState,
outbox: OutboxState,
pmConversations: PmConversationsState,
Expand Down
Loading