diff --git a/src/__tests__/lib/exampleData.js b/src/__tests__/lib/exampleData.js index 123b0ecda58..4cb0ccb35e8 100644 --- a/src/__tests__/lib/exampleData.js +++ b/src/__tests__/lib/exampleData.js @@ -21,6 +21,7 @@ import { randString, randInt } from '../../utils/misc'; import { makeUserId } from '../../api/idTypes'; import type { InitialData } from '../../api/apiTypes'; import { EventTypes, type UpdateMessageEvent } from '../../api/eventTypes'; +import { CreateWebPublicStreamPolicy } from '../../api/permissionsTypes'; import type { AccountSwitchAction, LoginSuccessAction, @@ -703,7 +704,7 @@ export const action = Object.freeze({ }): LoginSuccessAction), /** - * A minimal well-typed REGISTER_COMPLETE action. + * A minimal well-typed REGISTER_COMPLETE action from a recent server. * * Beware! The data here may not be representative. Generally each test * should specify the particular properties that it cares about. @@ -714,28 +715,44 @@ export const action = Object.freeze({ register_complete: (deepFreeze({ type: REGISTER_COMPLETE, data: { + // InitialDataBase last_event_id: 34, msg: '', queue_id: '1', + zulip_feature_level: 1, + zulip_version: zulipVersion.raw(), + + // InitialDataAlertWords alert_words: [], + + // InitialDataMessage max_message_id: 100, + + // InitialDataMutedTopics muted_topics: [], + + // InitialDataMutedUsers muted_users: [], + + // InitialDataPresence presences: {}, + + // InitialDataRealm development_environment: false, event_queue_longpoll_timeout_seconds: 600, + // jitsi_server_url omitted max_avatar_file_size_mib: 3, max_file_upload_size_mib: 3, - max_icon_file_size: 3, max_icon_file_size_mib: 3, max_logo_file_size_mib: 3, + max_message_length: 10000, + max_stream_description_length: 500, + max_stream_name_length: 100, + max_topic_length: 50, password_min_length: 8, password_min_guesses: 3, realm_add_custom_emoji_policy: 3, - realm_add_emoji_by_admins_only: true, - realm_allow_community_topic_editing: true, realm_allow_edit_history: true, - realm_allow_message_deleting: true, realm_allow_message_editing: true, realm_authentication_methods: { GitHub: true, Email: true, Google: true }, realm_available_video_chat_providers: {}, @@ -745,11 +762,10 @@ export const action = Object.freeze({ realm_community_topic_editing_limit_seconds: 600, realm_create_private_stream_policy: 3, realm_create_public_stream_policy: 3, - realm_create_web_public_stream_policy: 3, + realm_create_web_public_stream_policy: CreateWebPublicStreamPolicy.ModeratorOrAbove, realm_default_code_block_language: 'python', realm_default_external_accounts: {}, realm_default_language: 'en', - realm_default_twenty_four_hour_time: true, realm_delete_own_message_policy: 3, realm_description: 'description', realm_digest_emails_enabled: true, @@ -760,12 +776,12 @@ export const action = Object.freeze({ realm_email_auth_enabled: true, realm_email_changes_disabled: true, realm_emails_restricted_to_domains: false, + realm_enable_spectator_access: true, realm_giphy_rating: 3, realm_icon_source: 'U', realm_icon_url: 'example.com/some/path', realm_inline_image_preview: true, realm_inline_url_embed_preview: true, - realm_invite_by_admins_only: true, realm_invite_required: true, realm_invite_to_realm_policy: 3, realm_invite_to_stream_policy: 3, @@ -797,26 +813,29 @@ export const action = Object.freeze({ realm_waiting_period_threshold: 3, realm_wildcard_mention_policy: 3, server_avatar_changes_disabled: false, + server_generation: 3, + server_inline_image_preview: true, + server_inline_url_embed_preview: true, server_name_changes_disabled: false, server_needs_upgrade: false, - server_inline_url_embed_preview: true, - server_inline_image_preview: true, - server_generation: 3, + server_web_public_streams_enabled: true, settings_send_digest_emails: true, upgrade_text_for_wide_organization_logo: '', zulip_plan_is_not_limited: false, - zulip_feature_level: 1, - zulip_version: zulipVersion.raw(), + + // InitialDataRealmEmoji realm_emoji: {}, + + // InitialDataRealmFilters realm_filters: [], - // RawInitialDataRealmUser + // InitialDataRealmUser realm_users: [], realm_non_active_users: [], avatar_source: 'G', avatar_url_medium: 'url', avatar_url: null, // ideally would agree with selfUser.avatar_url - can_create_streams: false, + can_create_streams: false, // new servers don't send, but we fill in can_create_public_streams: false, can_create_private_streams: false, can_create_web_public_streams: false, @@ -833,19 +852,30 @@ export const action = Object.freeze({ full_name: selfUser.full_name, cross_realm_bots: [], + // InitialDataRealmUserGroups realm_user_groups: [], + + // InitialDataRecentPmConversations recent_private_conversations: [], + + // InitialDataStream streams: [], + + // InitialDataSubscription never_subscribed: [], subscriptions: [], unsubscribed: [], + + // InitialDataUpdateMessageFlags unread_msgs: { streams: [], huddles: [], - count: 0, pms: [], mentions: [], + count: 0, }, + + // InitialDataUserSettings user_settings: { twenty_four_hour_time: true, dense_mode: true, @@ -890,6 +920,8 @@ export const action = Object.freeze({ send_stream_typing_notifications: true, send_read_receipts: true, }, + + // InitialDataUserStatus user_status: {}, }, }): RegisterCompleteAction), diff --git a/src/__tests__/permissionSelectors-test.js b/src/__tests__/permissionSelectors-test.js new file mode 100644 index 00000000000..c2254d8d885 --- /dev/null +++ b/src/__tests__/permissionSelectors-test.js @@ -0,0 +1,130 @@ +/* @flow strict-local */ +import invariant from 'invariant'; + +import { tryGetActiveAccountState } from '../selectors'; +import { + Role, + type RoleT, + CreateWebPublicStreamPolicy, + type CreateWebPublicStreamPolicyT, +} from '../api/permissionsTypes'; +import { getCanCreateWebPublicStreams } from '../permissionSelectors'; +import rootReducer from '../boot/reducers'; +import { EVENT_USER_UPDATE } from '../actionConstants'; +import * as eg from './lib/exampleData'; + +describe('getCanCreateWebPublicStreams', () => { + const { AdminOrAbove, ModeratorOrAbove, Nobody, OwnerOnly } = CreateWebPublicStreamPolicy; + const { Owner, Admin, Moderator, Member, Guest } = Role; + + test('returns false when webPublicStreamsEnabled is false', () => { + const globalState = [ + { + type: EVENT_USER_UPDATE, + id: 0, + userId: eg.selfUser.user_id, + person: { user_id: eg.selfUser.user_id, role: Owner }, + }, + ].reduce( + rootReducer, + eg.reduxStatePlus({ + realm: { + ...eg.plusReduxState.realm, + webPublicStreamsEnabled: false, + enableSpectatorAccess: true, + createWebPublicStreamPolicy: ModeratorOrAbove, + }, + }), + ); + + const state = tryGetActiveAccountState(globalState); + invariant(state, 'expected per-account state'); + + expect(getCanCreateWebPublicStreams(state)).toBeFalse(); + }); + + test('returns false when enableSpectatorAccess is false', () => { + const globalState = [ + { + type: EVENT_USER_UPDATE, + id: 0, + userId: eg.selfUser.user_id, + person: { user_id: eg.selfUser.user_id, role: Owner }, + }, + ].reduce( + rootReducer, + eg.reduxStatePlus({ + realm: { + ...eg.plusReduxState.realm, + webPublicStreamsEnabled: true, + enableSpectatorAccess: false, + createWebPublicStreamPolicy: ModeratorOrAbove, + }, + }), + ); + + const state = tryGetActiveAccountState(globalState); + invariant(state, 'expected per-account state'); + + expect(getCanCreateWebPublicStreams(state)).toBeFalse(); + }); + + test.each` + policy | role | expected + ${AdminOrAbove} | ${Owner} | ${true} + ${AdminOrAbove} | ${Admin} | ${true} + ${AdminOrAbove} | ${Moderator} | ${false} + ${AdminOrAbove} | ${Member} | ${false} + ${AdminOrAbove} | ${Guest} | ${false} + ${ModeratorOrAbove} | ${Owner} | ${true} + ${ModeratorOrAbove} | ${Admin} | ${true} + ${ModeratorOrAbove} | ${Moderator} | ${true} + ${ModeratorOrAbove} | ${Member} | ${false} + ${ModeratorOrAbove} | ${Guest} | ${false} + ${Nobody} | ${Owner} | ${false} + ${Nobody} | ${Admin} | ${false} + ${Nobody} | ${Moderator} | ${false} + ${Nobody} | ${Member} | ${false} + ${Nobody} | ${Guest} | ${false} + ${OwnerOnly} | ${Owner} | ${true} + ${OwnerOnly} | ${Admin} | ${false} + ${OwnerOnly} | ${Moderator} | ${false} + ${OwnerOnly} | ${Member} | ${false} + ${OwnerOnly} | ${Guest} | ${false} + `( + 'returns $expected when policy is $policy and role is $role', + async ({ + policy, + role, + expected, + }: { + policy: CreateWebPublicStreamPolicyT, + role: RoleT, + expected: boolean, + }) => { + const globalState = [ + { + type: EVENT_USER_UPDATE, + id: 0, + userId: eg.selfUser.user_id, + person: { user_id: eg.selfUser.user_id, role }, + }, + ].reduce( + rootReducer, + eg.reduxStatePlus({ + realm: { + ...eg.plusReduxState.realm, + webPublicStreamsEnabled: true, + enableSpectatorAccess: true, + createWebPublicStreamPolicy: policy, + }, + }), + ); + + const state = tryGetActiveAccountState(globalState); + invariant(state, 'expected per-account state'); + + expect(getCanCreateWebPublicStreams(state)).toBe(expected); + }, + ); +}); diff --git a/src/api/apiTypes.js b/src/api/apiTypes.js index 141d8eac372..3728ca3e97d 100644 --- a/src/api/apiTypes.js +++ b/src/api/apiTypes.js @@ -4,3 +4,4 @@ export type * from './transportTypes'; export type * from './modelTypes'; export type * from './eventTypes'; export type * from './initialDataTypes'; +export type * from './permissionsTypes'; diff --git a/src/api/initialDataTypes.js b/src/api/initialDataTypes.js index c72cdf05f5b..e797f57cb89 100644 --- a/src/api/initialDataTypes.js +++ b/src/api/initialDataTypes.js @@ -16,6 +16,7 @@ import type { UserGroup, UserId, } from './apiTypes'; +import type { CreateWebPublicStreamPolicyT } from './permissionsTypes'; /* The types in this file are organized by which `fetch_event_types` values @@ -163,8 +164,9 @@ export type InitialDataRealm = $ReadOnly<{| // realm_create_private_stream_policy and realm_create_public_stream_policy realm_create_stream_policy?: number, - // TODO(server-5.0): Added in feat. 103; when absent, treat as 6 (nobody). - realm_create_web_public_stream_policy?: number, + // TODO(server-5.0): Added in feat. 103; when absent, treat as + // CreateWebPublicStreamPolicy.Nobody. + realm_create_web_public_stream_policy?: CreateWebPublicStreamPolicyT, realm_default_code_block_language: string | null, realm_default_external_accounts: {| diff --git a/src/api/modelTypes.js b/src/api/modelTypes.js index f4f754bf1ec..9ceafdca66f 100644 --- a/src/api/modelTypes.js +++ b/src/api/modelTypes.js @@ -6,6 +6,7 @@ import type { AvatarURL } from '../utils/avatar'; import type { UserId } from './idTypes'; +import type { RoleT } from './permissionsTypes'; export type * from './idTypes'; @@ -125,7 +126,7 @@ export type User = {| +bot_owner?: string, // TODO(server-4.0): New in FL 59 - +role?: number, + +role?: RoleT, // The ? is for future-proofing. Greg explains in 2020-02, at // https://github.com/zulip/zulip-mobile/pull/3789#discussion_r378554698 , @@ -212,7 +213,7 @@ export type CrossRealmBot = {| // +bot_owner?: string, // TODO(server-4.0): New in FL 59 - +role?: number, + +role?: RoleT, // The ? is for future-proofing. For bots it's always '': // https://github.com/zulip/zulip-mobile/pull/3789#issuecomment-581218576 diff --git a/src/api/permissionsTypes.js b/src/api/permissionsTypes.js new file mode 100644 index 00000000000..5b4fefca857 --- /dev/null +++ b/src/api/permissionsTypes.js @@ -0,0 +1,78 @@ +/* @flow strict-local */ + +import { typesEquivalent } from '../generics'; +import { objectValues } from '../flowPonyfill'; + +/** + * Organization-level role of a user. + */ +// Ideally both the type and enum would have the simple name; but a type +// and value sharing a name is one nice TS feature that Flow lacks. +// Or we could leapfrog TS and make this a Flow enum: +// https://flow.org/en/docs/enums/ +// (TS has its own enums, but they are a mess.) +// eslint-disable-next-line flowtype/type-id-match +export type RoleT = 100 | 200 | 300 | 400 | 600; + +/** + * An enum of all valid values for `RoleT`. + * + * Described in the server API doc, e.g., at `.role` in `realm_users` at + * https://zulip.com/api/register-queue + * + * See RoleValues for the list of values. + */ +export const Role = { + Owner: (100: 100), + Admin: (200: 200), + Moderator: (300: 300), + Member: (400: 400), + Guest: (600: 600), +}; + +// Check that the enum indeed has all and exactly the values of the type. +typesEquivalent>(); + +/** + * A list of all valid values for `RoleT`. + * + * See Role for an enum to refer to these by meaningful names. + */ +export const RoleValues: $ReadOnlyArray = objectValues(Role); + +/** + * The policy for which users can create web public streams in this + * organization. Ignore if the server hasn't opted into the concept of + * web-public streams. + */ +// Ideally both the type and enum would have the simple name; but a type +// and value sharing a name is one nice TS feature that Flow lacks. +// Or we could leapfrog TS and make this a Flow enum: +// https://flow.org/en/docs/enums/ +// (TS has its own enums, but they are a mess.) +// eslint-disable-next-line flowtype/type-id-match +export type CreateWebPublicStreamPolicyT = 2 | 4 | 6 | 7; + +/** + * An enum of all valid values for `CreateWebPublicStreamPolicyT`. + * + * See CreateWebPublicStreamPolicyValues for the list of values. + */ +export const CreateWebPublicStreamPolicy = { + AdminOrAbove: (2: 2), + ModeratorOrAbove: (4: 4), + Nobody: (6: 6), + OwnerOnly: (7: 7), +}; + +// Check that the enum indeed has all and exactly the values of the type. +typesEquivalent>(); + +/** + * A list of all valid values for `CreateWebPublicStreamPolicyT`. + * + * See CreateWebPublicStreamPolicy for an enum to refer to these by meaningful names. + */ +export const CreateWebPublicStreamPolicyValues: $ReadOnlyArray = objectValues( + CreateWebPublicStreamPolicy, +); diff --git a/src/emoji/data.js b/src/emoji/data.js index cc5ad61d009..4e883f71d9b 100644 --- a/src/emoji/data.js +++ b/src/emoji/data.js @@ -5,7 +5,7 @@ import type { EmojiType, ReactionType, EmojiForShared } from '../types'; import { objectFromEntries } from '../jsBackport'; import { unicodeCodeByName, override } from './codePointMap'; import zulipExtraEmojiMap from './zulipExtraEmojiMap'; -import objectEntries from '../utils/objectEntries'; +import { objectEntries } from '../flowPonyfill'; const unicodeEmojiNames = Object.keys(unicodeCodeByName); diff --git a/src/flowPonyfill.js b/src/flowPonyfill.js new file mode 100644 index 00000000000..e13d5a446cf --- /dev/null +++ b/src/flowPonyfill.js @@ -0,0 +1,29 @@ +/** + * Ponyfills for better-typed versions of functions than the Flow stdlib. + * + * FlowIssue: Everything here should really become a fix upstream in + * flowlib. + * + * @flow strict + */ + +/** + * Ponyfill for properly-typed `Object.entries`. + * + * See also `objectValues`, for `Object.values`. + */ +export function objectEntries(obj: {| +[K]: V |}): Array<[K, V]> { + // Flow's definition for Object.entries is simply + // static entries(object: mixed): Array<[string, mixed]>; + // .... which is almost useless. + return (Object.entries(obj): $FlowIssue); +} + +/** + * Ponyfill for properly-typed `Object.values`. + * + * See also `objectEntries`, for `Object.entries`. + */ +export function objectValues(obj: {| +[K]: V |}): V[] { + return (Object.values(obj): $FlowIssue); // Really should be fixed in flowlib. +} diff --git a/src/notification/__tests__/notification-test.js b/src/notification/__tests__/notification-test.js index fea165d1e65..056bdf77b96 100644 --- a/src/notification/__tests__/notification-test.js +++ b/src/notification/__tests__/notification-test.js @@ -9,7 +9,7 @@ import { topicNarrow, pm1to1NarrowFromUser, pmNarrowFromUsersUnsafe } from '../. import * as eg from '../../__tests__/lib/exampleData'; import { fromAPNsImpl as extractIosNotificationData } from '../extract'; -import objectEntries from '../../utils/objectEntries'; +import { objectEntries } from '../../flowPonyfill'; const realm_uri = eg.realm.toString(); const user_id = eg.selfUser.user_id; diff --git a/src/permissionSelectors.js b/src/permissionSelectors.js new file mode 100644 index 00000000000..c64be954e70 --- /dev/null +++ b/src/permissionSelectors.js @@ -0,0 +1,56 @@ +/* @flow strict-local */ +import type { PerAccountState } from './types'; +import { ensureUnreachable } from './types'; +import { Role } from './api/permissionsTypes'; +import { getRealm, getOwnUser } from './selectors'; + +/** + * Whether the self-user can create or edit a stream to be web-public. + * + * True just if: + * - the server has opted into the concept of web-public streams (see + * server_web_public_streams_enabled in + * https://zulip.com/api/register-queue), and + * - spectator access is enabled (see realm_enable_spectator_access in + * https://zulip.com/api/register-queue), and + * - the user's role is high enough, according to the realm's policy (see + * realm_users[].role and realm_create_web_public_stream_policy in + * https://zulip.com/api/register-queue) + * + * Like user_can_create_web_public_streams in the web-app's + * static/js/settings_data.ts. + */ +export function getCanCreateWebPublicStreams(state: PerAccountState): boolean { + const { webPublicStreamsEnabled, enableSpectatorAccess, createWebPublicStreamPolicy } = getRealm( + state, + ); + const ownUser = getOwnUser(state); + const { role } = ownUser; + + if (!webPublicStreamsEnabled || !enableSpectatorAccess) { + return false; + } + + switch (createWebPublicStreamPolicy) { + // FlowIssue: sad that we end up having to write numeric literals here :-/ + // But the most important thing to get from the type-checker here is + // that the ensureUnreachable works -- that ensures that when we add a + // new possible value, we'll add a case for it here. Couldn't find a + // cleaner way to write this that still accomplished that. Discussion: + // https://github.com/zulip/zulip-mobile/pull/5384#discussion_r875147220 + case 6: // CreateWebPublicStreamPolicy.Nobody + return false; + case 7: // CreateWebPublicStreamPolicy.OwnerOnly + return role === Role.Owner; + case 2: // CreateWebPublicStreamPolicy.AdminOrAbove + return role === Role.Owner || role === Role.Admin; + case 4: // CreateWebPublicStreamPolicy.ModeratorOrAbove + return role === Role.Owner || role === Role.Admin || role === Role.Moderator; + default: { + ensureUnreachable(createWebPublicStreamPolicy); + + // (Unreachable as long as the cases are exhaustive.) + return false; + } + } +} diff --git a/src/presence/presenceReducer.js b/src/presence/presenceReducer.js index 4f203e6def1..93c119f72ae 100644 --- a/src/presence/presenceReducer.js +++ b/src/presence/presenceReducer.js @@ -10,7 +10,7 @@ import { } from '../actionConstants'; import { NULL_OBJECT } from '../nullObjects'; import { getAggregatedPresence } from '../utils/presence'; -import objectEntries from '../utils/objectEntries'; +import { objectEntries } from '../flowPonyfill'; const initialState: PresenceState = NULL_OBJECT; diff --git a/src/realm/__tests__/realmReducer-test.js b/src/realm/__tests__/realmReducer-test.js index c5b9e066962..8268dcb884f 100644 --- a/src/realm/__tests__/realmReducer-test.js +++ b/src/realm/__tests__/realmReducer-test.js @@ -33,9 +33,9 @@ describe('realmReducer', () => { messageContentDeleteLimitSeconds: action.data.realm_message_content_delete_limit_seconds, messageContentEditLimitSeconds: action.data.realm_message_content_edit_limit_seconds, pushNotificationsEnabled: action.data.realm_push_notifications_enabled, - webPublicStreamsEnabled: action.data.server_web_public_streams_enabled ?? false, - createWebPublicStreamPolicy: action.data.realm_create_web_public_stream_policy ?? 6, - enableSpectatorAccess: action.data.realm_enable_spectator_access ?? false, + webPublicStreamsEnabled: action.data.server_web_public_streams_enabled, + createWebPublicStreamPolicy: action.data.realm_create_web_public_stream_policy, + enableSpectatorAccess: action.data.realm_enable_spectator_access, // // InitialDataRealmUser diff --git a/src/realm/realmReducer.js b/src/realm/realmReducer.js index 58c3bbda9b3..914edfb9b85 100644 --- a/src/realm/realmReducer.js +++ b/src/realm/realmReducer.js @@ -5,6 +5,7 @@ import type { RealmEmojiById, VideoChatProvider, } from '../types'; +import { CreateWebPublicStreamPolicy } from '../api/permissionsTypes'; import { EventTypes } from '../api/eventTypes'; import { REGISTER_COMPLETE, @@ -34,7 +35,7 @@ const initialState = { messageContentEditLimitSeconds: 1, pushNotificationsEnabled: true, webPublicStreamsEnabled: false, - createWebPublicStreamPolicy: 6, + createWebPublicStreamPolicy: CreateWebPublicStreamPolicy.Nobody, enableSpectatorAccess: false, // @@ -108,7 +109,8 @@ export default ( messageContentEditLimitSeconds: action.data.realm_message_content_edit_limit_seconds, pushNotificationsEnabled: action.data.realm_push_notifications_enabled, webPublicStreamsEnabled: action.data.server_web_public_streams_enabled ?? false, - createWebPublicStreamPolicy: action.data.realm_create_web_public_stream_policy ?? 6, + createWebPublicStreamPolicy: + action.data.realm_create_web_public_stream_policy ?? CreateWebPublicStreamPolicy.Nobody, enableSpectatorAccess: action.data.realm_enable_spectator_access ?? false, // @@ -173,6 +175,11 @@ export default ( if (data.description !== undefined) { result.description = data.description; } + // TODO: Can also update createWebPublicStreamPolicy? + // https://chat.zulip.org/#narrow/stream/412-api-documentation/topic/event.20with.20.60create_web_public_stream_policy.60.3F/near/1381274 + if (data.enable_spectator_access !== undefined) { + result.enableSpectatorAccess = data.enable_spectator_access; + } return result; } diff --git a/src/reduxTypes.js b/src/reduxTypes.js index 7165a5a6dfb..367f93fe51b 100644 --- a/src/reduxTypes.js +++ b/src/reduxTypes.js @@ -24,6 +24,7 @@ import type { UserGroup, UserId, UserPresence, + CreateWebPublicStreamPolicyT, } from './api/apiTypes'; import type { PerAccountSessionState, @@ -288,7 +289,7 @@ export type RealmState = $ReadOnly<{| messageContentEditLimitSeconds: number, pushNotificationsEnabled: boolean, webPublicStreamsEnabled: boolean, - createWebPublicStreamPolicy: number, + createWebPublicStreamPolicy: CreateWebPublicStreamPolicyT, enableSpectatorAccess: boolean, // diff --git a/src/storage/__tests__/storage-test.js b/src/storage/__tests__/storage-test.js index 8549a927daf..7a779061dc2 100644 --- a/src/storage/__tests__/storage-test.js +++ b/src/storage/__tests__/storage-test.js @@ -2,7 +2,7 @@ import invariant from 'invariant'; import * as eg from '../../__tests__/lib/exampleData'; -import objectEntries from '../../utils/objectEntries'; +import { objectEntries } from '../../flowPonyfill'; import type { GlobalState } from '../../types'; import CompressedAsyncStorage from '../CompressedAsyncStorage'; import { stringify, parse } from '../replaceRevive'; diff --git a/src/user-statuses/userStatusesModel.js b/src/user-statuses/userStatusesModel.js index 56554ace437..4e93abe4832 100644 --- a/src/user-statuses/userStatusesModel.js +++ b/src/user-statuses/userStatusesModel.js @@ -3,7 +3,7 @@ import Immutable from 'immutable'; import type { UserStatus, UserStatusUpdate } from '../api/modelTypes'; import { makeUserId } from '../api/idTypes'; -import objectEntries from '../utils/objectEntries'; +import { objectEntries } from '../flowPonyfill'; import type { PerAccountState, PerAccountApplicableAction, UserId } from '../types'; import type { UserStatusesState } from './userStatusesCore'; import { diff --git a/src/utils/logging.js b/src/utils/logging.js index 1182278b213..6525fd2d098 100644 --- a/src/utils/logging.js +++ b/src/utils/logging.js @@ -10,7 +10,7 @@ import { import type { ZulipVersion } from './zulipVersion'; import type { JSONable } from './jsonable'; -import objectEntries from './objectEntries'; +import { objectEntries } from '../flowPonyfill'; import config from '../config'; /** Type of "extras" intended for Sentry. */ diff --git a/src/utils/objectEntries.js b/src/utils/objectEntries.js deleted file mode 100644 index d6898002604..00000000000 --- a/src/utils/objectEntries.js +++ /dev/null @@ -1,12 +0,0 @@ -/* @flow strict */ - -/** Ponyfill for properly-typed `Object.entries`. */ - -// Flow's definition for Object.entries is simply -// static entries(object: mixed): Array<[string, mixed]>; -// .... which is almost useless. - -const objectEntries = (obj: {| +[K]: V |}): Array<[K, V]> => - (Object.entries(obj): $FlowFixMe); - -export default objectEntries; diff --git a/src/utils/presence.js b/src/utils/presence.js index 0736f830ab2..abd733198f2 100644 --- a/src/utils/presence.js +++ b/src/utils/presence.js @@ -5,7 +5,7 @@ import formatDistanceToNow from 'date-fns/formatDistanceToNow'; import type { ClientPresence, UserPresence, PresenceStatus, UserStatus } from '../types'; import { ensureUnreachable } from '../types'; -import objectEntries from './objectEntries'; +import { objectEntries } from '../flowPonyfill'; /** The relation `>=`, where `active` > `idle` > `offline`. */ const presenceStatusGeq = (a: PresenceStatus, b: PresenceStatus): boolean => { diff --git a/src/utils/url.js b/src/utils/url.js index 96fd4dac8d8..2f6974ef8d4 100644 --- a/src/utils/url.js +++ b/src/utils/url.js @@ -1,7 +1,7 @@ /* @flow strict-local */ import type { Auth } from '../api/transportTypes'; import { getAuthHeaders } from '../api/transport'; -import objectEntries from './objectEntries'; +import { objectEntries } from '../flowPonyfill'; /** * An object `encodeParamsForUrl` can flatten.