diff --git a/docs/howto/shared.md b/docs/howto/shared.md index ddcf0e444f3..4048195f81a 100644 --- a/docs/howto/shared.md +++ b/docs/howto/shared.md @@ -20,7 +20,7 @@ It's published to NPM as the package `@zulip/shared`. * To develop and test the shared code, use `yarn link` so that the shared code comes from your local zulip/zulip worktree (just like the mobile app code comes from your local zulip-mobile worktree) - rather than from NPM. See our [yarn-link.md][]. + rather than from NPM. See our [yarn-link.md](yarn-link.md). * For a new module `static/shared/js/foo.js`, you'll typically want to add a file `foo.js.flow` next to it with type definitions. See @@ -61,9 +61,10 @@ $ cd static/shared # the root of the @zulip/shared package's source $ git checkout master $ git pull --ff-only -$ npm version patch --no-git-tag-version \ - --message 'shared: Bump version to %s.' + # (These steps can probably become a `version` NPM script.) +$ npm version patch --no-git-tag-version # Suppose the new version is 0.0.3. Then: +$ git commit -am 'shared: Bump version to 0.0.3.' $ git tag shared-0.0.3 $ git log --stat -p upstream/master.. # check your work! diff --git a/docs/howto/yarn-link.md b/docs/howto/yarn-link.md index 02c7ef91724..812855393c5 100644 --- a/docs/howto/yarn-link.md +++ b/docs/howto/yarn-link.md @@ -45,6 +45,11 @@ $ readlink node_modules/@zulip/shared # ... to the worktree for the package source. $ readlink -f node_modules/@zulip/shared /home/greg/z/zulip/static/shared + + # Restart Flow to make it notice the symlink. + # (Then it'll automatically notice edits, as usual.) +$ npx flow stop && npx flow start + ``` When done, be sure to run `yarn unlink` to go back to letting the @@ -59,10 +64,12 @@ Quick reference for the second and subsequent time you do it: ``` # in your zulip-mobile clone $ yarn link @zulip/shared && yarn +$ npx flow stop && npx flow start # ... develop, test, etc. ... $ yarn unlink @zulip/shared && yarn install --force + # no need to restart Flow in this direction ``` ### Making our toolchain work diff --git a/package.json b/package.json index e091ac682c8..3672a4466a6 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@react-native-community/netinfo": "^5.9.5", "@sentry/react-native": "^1.0.9", "@unimodules/core": "~5.3.0", - "@zulip/shared": "^0.0.2", + "@zulip/shared": "^0.0.4", "base-64": "^0.1.0", "blueimp-md5": "^2.10.0", "color": "^3.0.0", diff --git a/src/api/modelTypes.js b/src/api/modelTypes.js index 7e9076882e4..2cf4777e2f9 100644 --- a/src/api/modelTypes.js +++ b/src/api/modelTypes.js @@ -296,21 +296,22 @@ export type Topic = {| // // -export type NarrowOperator = - | 'is' - | 'in' - | 'near' - | 'id' - | 'stream' - | 'topic' - | 'sender' - | 'pm-with' - | 'search'; - -export type NarrowElement = $ReadOnly<{| - operand: string, - operator: NarrowOperator, -|}>; +// See docs: https://zulip.com/api/construct-narrow +// prettier-ignore +/* eslint-disable semi-style */ +export type NarrowElement = + | {| +operator: 'is' | 'in' | 'topic' | 'search', +operand: string |} + // The server started accepting numeric user IDs and stream IDs in 2.1: + // * `stream` since 2.1-dev-2302-g3680393b4 + // * `group-pm-with` since 2.1-dev-1813-gb338fd130 + // * `sender` since 2.1-dev-1812-gc067c155a + // * `pm-with` since 2.1-dev-1350-gd7b4de234 + | {| +operator: 'stream', +operand: string | number |} // stream ID + | {| +operator: 'pm-with', +operand: string | $ReadOnlyArray |} // user IDs + | {| +operator: 'sender', +operand: string | number |} // user ID + | {| +operator: 'group-pm-with', +operand: string | number |} // user ID + | {| +operator: 'near' | 'id', +operand: number |} // message ID + ; /** * A narrow, in the form used in the Zulip API at get-messages. diff --git a/src/typing/__tests__/typingSelectors-test.js b/src/typing/__tests__/typingSelectors-test.js index fd060bf9374..22e3802675a 100644 --- a/src/typing/__tests__/typingSelectors-test.js +++ b/src/typing/__tests__/typingSelectors-test.js @@ -5,7 +5,7 @@ import { getCurrentTypingUsers } from '../typingSelectors'; import { HOME_NARROW, pm1to1NarrowFromUser, pmNarrowFromUsersUnsafe } from '../../utils/narrow'; import { NULL_ARRAY } from '../../nullObjects'; import * as eg from '../../__tests__/lib/exampleData'; -import { normalizeRecipientsAsUserIds } from '../../utils/recipient'; +import { pmTypingKeyFromPmKeyIds } from '../../utils/recipient'; describe('getCurrentTypingUsers', () => { test('return NULL_ARRAY when current narrow is not private or group', () => { @@ -33,25 +33,27 @@ describe('getCurrentTypingUsers', () => { test('when two people are typing, return details for all of them', () => { const user1 = eg.makeUser(); const user2 = eg.makeUser(); + const users = [user1, user2].sort((a, b) => a.user_id - b.user_id); + const userIds = users.map(u => u.user_id); - const normalizedRecipients = normalizeRecipientsAsUserIds([user1.user_id, user2.user_id]); + const normalizedRecipients = pmTypingKeyFromPmKeyIds(userIds); const state = eg.reduxState({ typing: { - [normalizedRecipients]: { userIds: [user1.user_id, user2.user_id] }, + [normalizedRecipients]: { userIds }, }, users: [user1, user2], }); - const typingUsers = getCurrentTypingUsers(state, pmNarrowFromUsersUnsafe([user1, user2])); + const typingUsers = getCurrentTypingUsers(state, pmNarrowFromUsersUnsafe(users)); - expect(typingUsers).toEqual([user1, user2]); + expect(typingUsers).toEqual(users); }); test('when in private narrow but different user is typing return NULL_ARRAY', () => { const user1 = eg.makeUser(); const user2 = eg.makeUser(); - const normalizedRecipients = normalizeRecipientsAsUserIds([user1.user_id]); + const normalizedRecipients = pmTypingKeyFromPmKeyIds([user1.user_id]); const state = eg.reduxState({ typing: { @@ -68,11 +70,9 @@ describe('getCurrentTypingUsers', () => { test('when in group narrow and someone is typing in that narrow return details', () => { const expectedUser = eg.makeUser(); const anotherUser = eg.makeUser(); + const users = [expectedUser, anotherUser].sort((a, b) => a.user_id - b.user_id); - const normalizedRecipients = normalizeRecipientsAsUserIds([ - expectedUser.user_id, - anotherUser.user_id, - ]); + const normalizedRecipients = pmTypingKeyFromPmKeyIds(users.map(u => u.user_id)); const state = eg.reduxState({ typing: { [normalizedRecipients]: { userIds: [expectedUser.user_id] }, @@ -80,10 +80,7 @@ describe('getCurrentTypingUsers', () => { users: [expectedUser, anotherUser], }); - const typingUsers = getCurrentTypingUsers( - state, - pmNarrowFromUsersUnsafe([expectedUser, anotherUser]), - ); + const typingUsers = getCurrentTypingUsers(state, pmNarrowFromUsersUnsafe(users)); expect(typingUsers).toEqual([expectedUser]); }); diff --git a/src/typing/typingReducer.js b/src/typing/typingReducer.js index a6371eb3f1f..f85d3af58da 100644 --- a/src/typing/typingReducer.js +++ b/src/typing/typingReducer.js @@ -9,7 +9,7 @@ import { LOGIN_SUCCESS, ACCOUNT_SWITCH, } from '../actionConstants'; -import { normalizeRecipientsAsUserIdsSansMe } from '../utils/recipient'; +import { pmTypingKeyFromRecipients } from '../utils/recipient'; import { NULL_OBJECT } from '../nullObjects'; const initialState: TypingState = NULL_OBJECT; @@ -20,7 +20,7 @@ const eventTypingStart = (state, action) => { return state; } - const normalizedRecipients: string = normalizeRecipientsAsUserIdsSansMe( + const normalizedRecipients: string = pmTypingKeyFromRecipients( action.recipients.map(r => r.user_id), action.ownUserId, ); @@ -47,7 +47,7 @@ const eventTypingStart = (state, action) => { }; const eventTypingStop = (state, action) => { - const normalizedRecipients: string = normalizeRecipientsAsUserIdsSansMe( + const normalizedRecipients: string = pmTypingKeyFromRecipients( action.recipients.map(r => r.user_id), action.ownUserId, ); diff --git a/src/typing/typingSelectors.js b/src/typing/typingSelectors.js index 61b928822cc..b50b02847bc 100644 --- a/src/typing/typingSelectors.js +++ b/src/typing/typingSelectors.js @@ -3,30 +3,22 @@ import { createSelector } from 'reselect'; import type { Narrow, Selector, UserOrBot } from '../types'; import { getTyping } from '../directSelectors'; -import { emailsOfPmNarrow, isPmNarrow } from '../utils/narrow'; -import { normalizeRecipientsAsUserIds } from '../utils/recipient'; +import { userIdsOfPmNarrow, isPmNarrow } from '../utils/narrow'; +import { pmTypingKeyFromPmKeyIds } from '../utils/recipient'; import { NULL_ARRAY, NULL_USER } from '../nullObjects'; -import { getAllUsersById, getAllUsersByEmail } from '../users/userSelectors'; +import { getAllUsersById } from '../users/userSelectors'; export const getCurrentTypingUsers: Selector<$ReadOnlyArray, Narrow> = createSelector( (state, narrow) => narrow, state => getTyping(state), state => getAllUsersById(state), - state => getAllUsersByEmail(state), - (narrow, typing, allUsersById, allUsersByEmail): UserOrBot[] => { + (narrow, typing, allUsersById): UserOrBot[] => { if (!isPmNarrow(narrow)) { return NULL_ARRAY; } - const recipients = emailsOfPmNarrow(narrow).map(email => { - const userId = allUsersByEmail.get(email)?.user_id; - if (userId === undefined) { - throw new Error(`Narrow contains email '${email}' that does not map to any user.`); - } - return userId; - }); - const normalizedRecipients = normalizeRecipientsAsUserIds(recipients); - const currentTyping = typing[normalizedRecipients]; + const typingKey = pmTypingKeyFromPmKeyIds(userIdsOfPmNarrow(narrow)); + const currentTyping = typing[typingKey]; if (!currentTyping || !currentTyping.userIds) { return NULL_ARRAY; diff --git a/src/users/usersActions.js b/src/users/usersActions.js index 3d44f4abdd3..9a66cfb5b60 100644 --- a/src/users/usersActions.js +++ b/src/users/usersActions.js @@ -5,8 +5,8 @@ import type { Auth, Dispatch, GetState, GlobalState, Narrow } from '../types'; import * as api from '../api'; import { PRESENCE_RESPONSE } from '../actionConstants'; import { getAuth, tryGetAuth, getServerVersion } from '../selectors'; -import { isPmNarrow, emailsOfPmNarrow } from '../utils/narrow'; -import { getAllUsersByEmail, getUserForId } from './userSelectors'; +import { isPmNarrow, userIdsOfPmNarrow } from '../utils/narrow'; +import { getUserForId } from './userSelectors'; import { ZulipVersion } from '../utils/zulipVersion'; export const reportPresence = (isActive: boolean = true, newUserInput: boolean = false) => async ( @@ -46,11 +46,11 @@ const typingWorker = (state: GlobalState) => { return { get_current_time: () => new Date().getTime(), - notify_server_start: (user_ids_array: number[]) => { + notify_server_start: (user_ids_array: $ReadOnlyArray) => { api.typing(auth, getRecipients(user_ids_array), 'start'); }, - notify_server_stop: (user_ids_array: number[]) => { + notify_server_stop: (user_ids_array: $ReadOnlyArray) => { api.typing(auth, getRecipients(user_ids_array), 'stop'); }, }; @@ -64,14 +64,7 @@ export const sendTypingStart = (narrow: Narrow) => async ( return; } - const allUsersByEmail = getAllUsersByEmail(getState()); - const recipientIds = emailsOfPmNarrow(narrow).map(email => { - const user = allUsersByEmail.get(email); - if (!user) { - throw new Error('unknown user'); - } - return user.user_id; - }); + const recipientIds = userIdsOfPmNarrow(narrow); typing_status.update(typingWorker(getState()), recipientIds); }; diff --git a/src/utils/narrow.js b/src/utils/narrow.js index 6b74854b1d7..cb810142e33 100644 --- a/src/utils/narrow.js +++ b/src/utils/narrow.js @@ -342,13 +342,13 @@ export const isGroupPmNarrow = (narrow?: Narrow): boolean => !!narrow && caseNarrowDefault(narrow, { pm: (emails, ids) => ids.length > 1 }, () => false); /** - * The "PM key recipients" emails for a PM narrow; else error. + * The "PM key recipients" IDs for a PM narrow; else error. * * This is the same list of users that can appear in a `PmKeyRecipients` or - * `PmKeyUsers`, but contains only their emails. + * `PmKeyUsers`, but contains only their user IDs. */ -export const emailsOfPmNarrow = (narrow: Narrow): $ReadOnlyArray => - caseNarrowPartial(narrow, { pm: emails => emails }); +export const userIdsOfPmNarrow = (narrow: Narrow): $ReadOnlyArray => + caseNarrowPartial(narrow, { pm: (emails, ids) => ids }); /** * The stream name for a stream or topic narrow; else error. diff --git a/src/utils/recipient.js b/src/utils/recipient.js index 4553a5914ac..2df3b220af6 100644 --- a/src/utils/recipient.js +++ b/src/utils/recipient.js @@ -298,6 +298,43 @@ export const pmUnreadsKeyFromPmKeyIds = ( } }; +/** + * The key for a PM thread in typing-status data, given the IDs we use generally. + * + * This produces the key string we use in `state.typing`, given the list of + * users that `pmKeyRecipientsFromMessage` would provide and that we use in + * most of our other data structures indexed on narrows. + * + * See also `pmTypingKeyFromRecipients`. + */ +// That key string is: just the usual "PM key" list of users, stringified +// and comma-separated. +// +// TODO: It'd be neat to have another opaque type like PmKeyIds, for this +// and pmUnreadsKeyFromPmKeyIds to consume. Perhaps simplest to do after +// Narrow no longer contains emails. +export const pmTypingKeyFromPmKeyIds = (userIds: $ReadOnlyArray): string => + userIds.join(','); + +/** + * The key for a PM thread in typing-status data, given a recipients list. + * + * This produces the key string we use in `state.typing`, given the list of + * users that a typing-status event provides in `recipients`. + * + * See also `pmTypingKeyFromPmKeyIds`. + */ +// This implementation works because: +// * For all but self-PMs, we want the list of non-self users, which +// `filterRecipientsAsUserIds` will give regardless of whether self was +// in the input. (So it doesn't matter what convention the server uses +// for these events.) +// * Self-PMs don't have typing-status events in the first place. +export const pmTypingKeyFromRecipients = ( + recipients: $ReadOnlyArray, + ownUserId: number, +): string => pmTypingKeyFromPmKeyIds(filterRecipientsAsUserIds(recipients, ownUserId)); + export const isSameRecipient = ( message1: Message | Outbox, message2: Message | Outbox, diff --git a/yarn.lock b/yarn.lock index dc7945f69e3..59187615633 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2725,12 +2725,13 @@ resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== -"@zulip/shared@^0.0.2": - version "0.0.2" - resolved "https://registry.yarnpkg.com/@zulip/shared/-/shared-0.0.2.tgz#14613950551c96d88f7b8929056ac1b640f15343" - integrity sha512-Thts/Mu/9ry4M0K4RjL4Gym8MTxkcnk7lkBuH8hiMkIEbWoK5CaHSRxvn96wCsUJJcQ7A99Sd+RVQLPMorRLAg== +"@zulip/shared@^0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@zulip/shared/-/shared-0.0.4.tgz#147a939bd71a284ed6b890843f9e4a003466637e" + integrity sha512-qsSWfXSFUoaWDa8GeoIOKpQVGgs744DRZ2fspw52NeWa7Ac8GnhxIvu9EohBTmq6jApRIO5wg/8DkgnvsSqxyA== dependencies: - underscore "^1.9.1" + katex "^0.12.0" + lodash "^4.17.19" abab@^2.0.0, abab@^2.0.3: version "2.0.5" @@ -7857,6 +7858,13 @@ katex@^0.11.1: dependencies: commander "^2.19.0" +katex@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/katex/-/katex-0.12.0.tgz#2fb1c665dbd2b043edcf8a1f5c555f46beaa0cb9" + integrity sha512-y+8btoc/CK70XqcHqjxiGWBOeIL8upbS0peTPXTvgrh21n1RiWWcIpSWM+4uXq+IAgNh9YYQWdc7LVDPDAEEAg== + dependencies: + commander "^2.19.0" + katex@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/katex/-/katex-0.7.1.tgz#06bb5298efad05e1e7228035ba8e1591f3061b8f" @@ -11788,11 +11796,6 @@ ultron@1.0.x: resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" integrity sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po= -underscore@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" - integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg== - unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"