Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d4dc9c2
typing-status tests: Make test data less ad-hoc.
gnprice Dec 16, 2020
d11911a
recipient [nfc]: Mark an array parameter read-only.
gnprice Jan 14, 2021
a171a05
api: Cut unused, unimplemented user-groups bindings.
gnprice Jan 20, 2021
d4c57b6
api types [nfc]: Move import to above a section heading.
gnprice Dec 15, 2020
c4c39ca
api types: Add UserId, an opaque type alias; don't use it yet.
gnprice Dec 12, 2020
f59cd48
user types: Use the new UserId alias in exampleData.
gnprice Jan 14, 2021
62d48a7
user types: Use makeUserId in test code.
gnprice Dec 16, 2020
11f5b83
user types: Use makeUserId when parsing a user ID from a string.
gnprice Jan 14, 2021
1170499
user types: Use makeUserId in our fake-yet-not-test data.
gnprice Jan 14, 2021
31e0e3f
api types: Use UserId in most of the API.
gnprice Dec 12, 2020
075d971
user types: Use UserId for PM recipients.
gnprice Jan 14, 2021
19c8189
user types: Use UserId for "own user ID" data.
gnprice Jan 20, 2021
15ab4d8
user types: Use UserId in initial PM-conversations data.
gnprice Jan 14, 2021
78cdd5e
user types: Use UserId in user-status data.
gnprice Jan 14, 2021
109bc35
user types: Use UserId for unreads data.
gnprice Jan 14, 2021
b3b2599
user types: Use UserId for typing-status state.
gnprice Jan 20, 2021
b950e25
user types: Use UserId in MentionWarnings component.
gnprice Jan 14, 2021
91b8572
user types: Switch to UserId in most React props and state.
gnprice Dec 12, 2020
9f53018
user types: Use UserId in pmKeyRecipientsFromIds.
gnprice Jan 20, 2021
3219710
user types: Propagate UserId type through outbound typing-status.
gnprice Jan 14, 2021
9d12a96
user types: Consume UserId in two user-data selectors.
gnprice Jan 20, 2021
a8b3a21
user types: Switch to UserId in getAllUsersById and friends.
gnprice Jan 20, 2021
7fb49af
user types: Use UserId for sending a PM, in sharing code.
gnprice Jan 20, 2021
1680f7b
api types: Require UserId on input to the API, completing the convers…
gnprice Jan 14, 2021
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
27 changes: 20 additions & 7 deletions src/__tests__/lib/exampleData.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import type {
Subscription,
User,
UserGroup,
UserId,
} from '../../api/modelTypes';
import { makeUserId } from '../../api/idTypes';
import type { Action, GlobalState, MessagesState, RealmState } from '../../reduxTypes';
import type { Auth, Account, Outbox } from '../../types';
import { UploadedAvatarURL } from '../../utils/avatar';
Expand Down Expand Up @@ -104,10 +106,12 @@ export const diverseCharacters =

type UserOrBotPropertiesArgs = {|
name?: string,
user_id?: number,
user_id?: number, // accept a plain number, for convenience in tests
|};

const randUserId: () => number = makeUniqueRandInt('user IDs', 10000);
const randUserId: () => UserId = (mk => () => makeUserId(mk()))(
makeUniqueRandInt('user IDs', 10000),
);
const userOrBotProperties = ({ name: _name, user_id }: UserOrBotPropertiesArgs) => {
const name = _name ?? randString();
const capsName = name.substring(0, 1).toUpperCase() + name.substring(1);
Expand All @@ -125,7 +129,7 @@ const userOrBotProperties = ({ name: _name, user_id }: UserOrBotPropertiesArgs)
full_name: `${capsName} User`,
is_admin: false,
timezone: 'UTC',
user_id: user_id ?? randUserId(),
user_id: user_id != null ? makeUserId(user_id) : randUserId(),
});
};

Expand Down Expand Up @@ -321,11 +325,16 @@ export const pmMessage = (args?: {|
...$Rest<Message, {}>,
sender?: User,
recipients?: User[],
sender_id?: number, // accept a plain number, for convenience in tests
|}): Message => {
// The `Object.freeze` is to work around a Flow issue:
// https://github.com/facebook/flow/issues/2386#issuecomment-695064325
const { sender = otherUser, recipients = [otherUser, selfUser], ...extra } =
args ?? Object.freeze({});
const {
sender = otherUser,
recipients = [otherUser, selfUser],
sender_id = undefined,
...extra
} = args ?? Object.freeze({});

const baseMessage: Message = {
...messagePropertiesBase,
Expand All @@ -344,7 +353,11 @@ export const pmMessage = (args?: {|
type: 'private',
};

return deepFreeze({ ...baseMessage, ...extra });
return deepFreeze({
...baseMessage,
...(sender_id != null && { sender_id: makeUserId(sender_id) }),
...extra,
});
};

export const pmMessageFromTo = (from: User, to: User[], extra?: $Rest<Message, {}>): Message =>
Expand Down Expand Up @@ -537,7 +550,7 @@ export const action = deepFreeze({
is_admin: false,
realm_non_active_users: [],
realm_users: [],
user_id: 4,
user_id: makeUserId(4),
realm_user_groups: [],
recent_private_conversations: [],
streams: [],
Expand Down
23 changes: 12 additions & 11 deletions src/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import type {
CaughtUpState,
MuteState,
AlertWordsState,
UserId,
UserStatusEvent,
} from './types';
import type { ZulipVersion } from './utils/zulipVersion';
Expand Down Expand Up @@ -218,7 +219,7 @@ type MessageFetchCompleteAction = {|
numAfter: number,
foundNewest: boolean | void,
foundOldest: boolean | void,
ownUserId: number,
ownUserId: UserId,
|};

type InitialFetchStartAction = {|
Expand Down Expand Up @@ -285,15 +286,15 @@ type EventSubscriptionPeerAddAction = {|
type: typeof EVENT_SUBSCRIPTION,
op: 'peer_add',
subscriptions: string[],
user_id: number,
user_id: UserId,
|};

type EventSubscriptionPeerRemoveAction = {|
...ServerEvent,
type: typeof EVENT_SUBSCRIPTION,
op: 'peer_remove',
subscriptions: string[],
user_id: number,
user_id: UserId,
|};

type GenericEventAction = {|
Expand Down Expand Up @@ -345,7 +346,7 @@ type EventUpdateMessageAction = {|
rendered_content: string,
subject_links: string[],
subject: string,
user_id: number,
user_id: UserId,
|};

type EventReactionCommon = {|
Expand Down Expand Up @@ -373,13 +374,13 @@ type EventPresenceAction = {|

type EventTypingCommon = {|
...ServerEvent,
ownUserId: number,
recipients: Array<{
user_id: number,
ownUserId: UserId,
recipients: $ReadOnlyArray<{
user_id: UserId,
email: string,
}>,
sender: {
user_id: number,
user_id: UserId,
email: string,
},
time: number,
Expand Down Expand Up @@ -422,7 +423,7 @@ type EventUserRemoveAction = {|
type EventUserUpdateAction = {|
...ServerEvent,
type: typeof EVENT_USER_UPDATE,
userId: number,
userId: UserId,
// Include only the fields that should be overwritten.
person: $Shape<User>,
|};
Expand Down Expand Up @@ -460,15 +461,15 @@ type EventUserGroupAddMembersAction = {|
type: typeof EVENT_USER_GROUP_ADD_MEMBERS,
op: 'add_members',
group_id: number,
user_ids: number[],
user_ids: UserId[],
|};

type EventUserGroupRemoveMembersAction = {|
...ServerEvent,
type: typeof EVENT_USER_GROUP_REMOVE_MEMBERS,
op: 'remove_members',
group_id: number,
user_ids: number[],
user_ids: UserId[],
|};

type EventRealmEmojiUpdateAction = {|
Expand Down
6 changes: 3 additions & 3 deletions src/api/eventTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* @flow strict-local
*/

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

export class EventTypes {
static alert_words: 'alert_words' = 'alert_words';
Expand Down Expand Up @@ -75,7 +75,7 @@ export type SubmessageEvent = {|
type: typeof EventTypes.submessage,
submessage_id: number,
message_id: number,
sender_id: number,
sender_id: UserId,
msg_type: 'widget',
content: string,
|};
Expand Down Expand Up @@ -103,7 +103,7 @@ export type PresenceEvent = {|
export type UserStatusEvent = {|
...EventCommon,
type: typeof EventTypes.user_status,
user_id: number,
user_id: UserId,
away?: boolean,
status_text?: string,
|};
Expand Down
54 changes: 54 additions & 0 deletions src/api/idTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* @flow strict-local */

/**
* A user ID.
*
* This is a number that identifies a particular Zulip user. Different
* users on the same Zulip server will have different user IDs. On the
* other hand, between different Zulip servers the same user ID may be used
* to refer to completely unrelated users.
*
* In general, if something calls for a value of this type, then you should
* be getting it from something that already has this type: like the
* `user_id` property of a `User` object, or a data structure that stores
* user IDs.
*
* The only other way to create a `UserId` is to call the `makeUserId`
* function provided by this module.
*
* See also type `User`, for the thing that one of these identifies.
*/
// How this type works: it's an "opaque type alias" for simply a number.
// See Flow docs: https://flow.org/en/docs/types/opaque-types/
//
// This means that:
// * At runtime, a `UserId` value is just a number. The use of `UserId`
// involves zero runtime overhead compared with simply `number`.
// * Because we've written a "bound" of `: number`, all code that has one
// of these values can freely use it as if it were simply `number`.
// * On the other hand, the only way to *create* such a value is to invoke
// something from this module to do it for you.
//
// For more background discussion of opaque types, see `PmKeyRecipients`.
export opaque type UserId: number = number;

/**
* Take a number, and declare that it truly is a user ID.
*
* This does nothing at all at runtime, just returning the value it's
* passed. Its only effect is to inform the type-checker that it's OK to
* use this value where a user ID is required.
*
* In general the only legitimate use case for this function, outside of
* tests, is when parsing a user ID from a string. When getting a user ID
* from any other source, if the values really are user IDs then the type of
* that source should be adjusted to say so.
*/
export const makeUserId = (id: number): UserId => id;

/* Possible future work:
Copy link
Copy Markdown
Collaborator

@chrisbobbe chrisbobbe Jan 20, 2021

Choose a reason for hiding this comment

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

Possible future work:

Makes sense. Do you think it'll always be pretty clear whether a given ID type belongs here in src/api/idTypes.js, vs. somewhere else?

I'm thinking of the return value of keyFromNarrow, which we've discussed making an opaque type for. I think that pretty clearly belongs right alongside keyFromNarrow, in narrows.js. I think I'd say it doesn't belong in src/api because we never pass those keys to the API, and we never receive them from the API. We do pass the result of apiNarrowOfNarrow to the API.

I think this is a sensible criterion for putting ID types in src/api/idTypes.js: if we ever talk to the API using that type of ID, its opaque type should live here. Perhaps that goes without saying, but we do a lot of passing-around of IDs within the app (including of user IDs, stream IDs, and our own narrow keys), so I wonder if a small comment might be helpful. 🙂

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The basic principle is that src/api/ is self-contained, so that code there shouldn't import code elsewhere -- it's describing the server API, and behaves like an independent library for doing that. We've made a few exceptions to that principle for convenience, but are generally pretty consistent about it.

So that means that any type that's going to be used in the API should be defined in the API part of the code. If we introduce a StreamId or MessageId, we'll want to use it in the API, so it'll be defined there.

The narrow-keys returned by keyFromNarrow don't appear in the API; they're our own invention for within the app. So they naturally wouldn't be defined in src/api/, and instead would appear in narrows.js as you say.

Because this is a more general principle I don't think it calls for a comment on this specific instance. But it'd be good to write down that general principle, which I'm not sure we have. Any suggestions on where would be most helpful?

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.

Makes sense, thanks! I've just merged ba7108f, wiki-style.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Looks good, thanks!

export opaque type StreamId: number = number;
export opaque type MessageId: number = number;
export const makeStreamId = (id: number): StreamId => id;
export const makeMessageId = (id: number): MessageId => id;
*/
12 changes: 0 additions & 12 deletions src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,6 @@ import toggleStreamNotifications from './subscriptions/toggleStreamNotifications
import getSubscriptionToStream from './subscriptions/getSubscriptionToStream';
import unmuteTopic from './subscriptions/unmuteTopic';
import tryGetFileTemporaryUrl from './tryGetFileTemporaryUrl';
import createUserGroup from './user_groups/createUserGroup';
import deleteUserGroup from './user_groups/deleteUserGroup';
import editUserGroup from './user_groups/editUserGroup';
import editUserGroupMembers from './user_groups/editUserGroupMembers';
import getUserGroupById from './user_groups/getUserGroupById';
import getUserGroups from './user_groups/getUserGroups';
import getUsers from './users/getUsers';
import createUser from './users/createUser';
import getUserProfile from './users/getUserProfile';
Expand Down Expand Up @@ -104,12 +98,6 @@ export {
toggleStreamNotifications,
unmuteTopic,
tryGetFileTemporaryUrl,
createUserGroup,
deleteUserGroup,
editUserGroup,
editUserGroupMembers,
getUserGroupById,
getUserGroups,
getUsers,
createUser,
getUserProfile,
Expand Down
7 changes: 4 additions & 3 deletions src/api/initialDataTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
Subscription,
User,
UserGroup,
UserId,
UserPresence,
UserStatusMapObject,
} from './apiTypes';
Expand Down Expand Up @@ -113,7 +114,7 @@ export type RawInitialDataRealmUser = {|
is_admin: boolean,
realm_users: Array<{| ...User, avatar_url?: string | null |}>,
realm_non_active_users: Array<{| ...User, avatar_url?: string | null |}>,
user_id: number,
user_id: UserId,
|};

export type InitialDataRealmUser = {|
Expand Down Expand Up @@ -204,7 +205,7 @@ export type StreamUnreadItem = {|
unread_message_ids: number[],

/** All distinct senders of these messages; sorted. */
// sender_ids: number[],
// sender_ids: UserId[],
|};

export type HuddlesUnreadItem = {|
Expand All @@ -226,7 +227,7 @@ export type PmsUnreadItem = {|
* the normal thing even then would be to make a bot user to send the
* messages as.) See server commit ca74cd6e3.
*/
sender_id: number,
sender_id: UserId,

// Sorted.
unread_message_ids: number[],
Expand Down
4 changes: 2 additions & 2 deletions src/api/messages/getMessages.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { Auth, ApiResponseSuccess } from '../transportTypes';
import type { Identity } from '../../types';
import type { Message, ApiNarrow } from '../apiTypes';
import type { Reaction } from '../modelTypes';
import type { Reaction, UserId } from '../modelTypes';
import { apiGet } from '../apiFetch';
import { identityOfAuth } from '../../account/accountMisc';
import { AvatarURL } from '../../utils/avatar';
Expand All @@ -27,7 +27,7 @@ export type ServerReaction = $ReadOnly<{|
user: $ReadOnly<{|
email: string,
full_name: string,
id: number,
id: UserId,
|}>,
|}>;

Expand Down
Loading