diff --git a/lib/selectors/thread-selectors.js b/lib/selectors/thread-selectors.js index 67fd7d5b5..8819f1ac9 100644 --- a/lib/selectors/thread-selectors.js +++ b/lib/selectors/thread-selectors.js @@ -1,573 +1,590 @@ // @flow import _compact from 'lodash/fp/compact.js'; import _filter from 'lodash/fp/filter.js'; import _flow from 'lodash/fp/flow.js'; import _groupBy from 'lodash/fp/groupBy.js'; import _map from 'lodash/fp/map.js'; import _mapValues from 'lodash/fp/mapValues.js'; import _some from 'lodash/fp/some.js'; import _sortBy from 'lodash/fp/sortBy.js'; import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import { createObjectSelector } from 'reselect-map'; import { filteredThreadIDsSelector, includeDeletedSelector, } from './calendar-filter-selectors.js'; import { relativeMemberInfoSelectorForMembersOfThread } from './user-selectors.js'; import genesis from '../facts/genesis.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import { getAvatarForThread, getRandomDefaultEmojiAvatar, } from '../shared/avatar-utils.js'; import { createEntryInfo } from '../shared/entry-utils.js'; import { threadInHomeChatList, threadInBackgroundChatList, threadInFilterList, threadInfoFromRawThreadInfo, threadHasAdminRole, roleIsAdminRole, threadIsPending, getPendingThreadID, - pendingThreadType, } from '../shared/thread-utils.js'; import type { ClientAvatar, ClientEmojiAvatar } from '../types/avatar-types'; import type { EntryInfo } from '../types/entry-types.js'; import type { MessageStore } from '../types/message-types.js'; import type { RelativeMemberInfo, ThreadInfo, RawThreadInfo, ThickRawThreadInfo, } from '../types/minimally-encoded-thread-permissions-types.js'; import type { BaseAppState } from '../types/redux-types.js'; import { threadTypeIsThick, threadTypeIsCommunityRoot, type ThreadType, + threadTypes, threadTypeIsSidebar, + threadTypeIsPrivate, + threadTypeIsPersonal, + personalThreadTypes, + privateThreadTypes, + sidebarThreadTypes, } from '../types/thread-types-enum.js'; import type { MixedRawThreadInfos, RawThreadInfos, ThickRawThreadInfos, } from '../types/thread-types.js'; import { dateString, dateFromString } from '../utils/date-utils.js'; import { values } from '../utils/objects.js'; const _mapValuesWithKeys = _mapValues.convert({ cap: false }); type ThreadInfoSelectorType = (state: BaseAppState<>) => { +[id: string]: ThreadInfo, }; const threadInfoSelector: ThreadInfoSelectorType = createObjectSelector( (state: BaseAppState<>) => state.threadStore.threadInfos, (state: BaseAppState<>) => state.currentUserInfo && state.currentUserInfo.id, (state: BaseAppState<>) => state.userStore.userInfos, threadInfoFromRawThreadInfo, ); const communityThreadSelector: ( state: BaseAppState<>, ) => $ReadOnlyArray = createSelector( threadInfoSelector, (threadInfos: { +[id: string]: ThreadInfo }) => { const result = []; for (const threadID in threadInfos) { const threadInfo = threadInfos[threadID]; if (!threadTypeIsCommunityRoot(threadInfo.type)) { continue; } result.push(threadInfo); } return result; }, ); const canBeOnScreenThreadInfos: ( state: BaseAppState<>, ) => $ReadOnlyArray = createSelector( threadInfoSelector, (threadInfos: { +[id: string]: ThreadInfo }) => { const result = []; for (const threadID in threadInfos) { const threadInfo = threadInfos[threadID]; if (!threadInFilterList(threadInfo)) { continue; } result.push(threadInfo); } return result; }, ); const onScreenThreadInfos: ( state: BaseAppState<>, ) => $ReadOnlyArray = createSelector( filteredThreadIDsSelector, canBeOnScreenThreadInfos, ( inputThreadIDs: ?$ReadOnlySet, threadInfos: $ReadOnlyArray, ): $ReadOnlyArray => { const threadIDs = inputThreadIDs; if (!threadIDs) { return threadInfos; } return threadInfos.filter(threadInfo => threadIDs.has(threadInfo.id)); }, ); const entryInfoSelector: (state: BaseAppState<>) => { +[id: string]: EntryInfo, } = createObjectSelector( (state: BaseAppState<>) => state.entryStore.entryInfos, (state: BaseAppState<>) => state.currentUserInfo && state.currentUserInfo.id, (state: BaseAppState<>) => state.userStore.userInfos, createEntryInfo, ); // "current" means within startDate/endDate range, not deleted, and in // onScreenThreadInfos const currentDaysToEntries: (state: BaseAppState<>) => { +[dayString: string]: EntryInfo[], } = createSelector( entryInfoSelector, (state: BaseAppState<>) => state.entryStore.daysToEntries, (state: BaseAppState<>) => state.navInfo.startDate, (state: BaseAppState<>) => state.navInfo.endDate, onScreenThreadInfos, includeDeletedSelector, ( entryInfos: { +[id: string]: EntryInfo }, daysToEntries: { +[day: string]: string[] }, startDateString: string, endDateString: string, onScreen: $ReadOnlyArray, includeDeleted: boolean, ) => { const allDaysWithinRange: { [string]: string[] } = {}, startDate = dateFromString(startDateString), endDate = dateFromString(endDateString); for ( const curDate = startDate; curDate <= endDate; curDate.setDate(curDate.getDate() + 1) ) { allDaysWithinRange[dateString(curDate)] = []; } return _mapValuesWithKeys((_: string[], dayString: string) => _flow( _map((entryID: string) => entryInfos[entryID]), _compact, _filter( (entryInfo: EntryInfo) => (includeDeleted || !entryInfo.deleted) && _some(['id', entryInfo.threadID])(onScreen), ), _sortBy('creationTime'), )(daysToEntries[dayString] ? daysToEntries[dayString] : []), )(allDaysWithinRange); }, ); const childThreadInfos: (state: BaseAppState<>) => { +[id: string]: $ReadOnlyArray, } = createSelector( threadInfoSelector, (threadInfos: { +[id: string]: ThreadInfo }) => { const result: { [string]: ThreadInfo[], } = {}; for (const id in threadInfos) { const threadInfo = threadInfos[id]; const parentThreadID = threadInfo.parentThreadID; if (parentThreadID === null || parentThreadID === undefined) { continue; } if (result[parentThreadID] === undefined) { result[parentThreadID] = ([]: ThreadInfo[]); } result[parentThreadID].push(threadInfo); } return result; }, ); const containedThreadInfos: (state: BaseAppState<>) => { +[id: string]: $ReadOnlyArray, } = createSelector( threadInfoSelector, (threadInfos: { +[id: string]: ThreadInfo }) => { const result: { [string]: ThreadInfo[], } = {}; for (const id in threadInfos) { const threadInfo = threadInfos[id]; const { containingThreadID } = threadInfo; if (containingThreadID === null || containingThreadID === undefined) { continue; } if (result[containingThreadID] === undefined) { result[containingThreadID] = ([]: ThreadInfo[]); } result[containingThreadID].push(threadInfo); } return result; }, ); const thickRawThreadInfosSelector: ( state: BaseAppState<>, ) => ThickRawThreadInfos = createSelector( (state: BaseAppState<>) => state.threadStore.threadInfos, (threadInfos: RawThreadInfos): ThickRawThreadInfos => { const thickRawThreadInfos: { [id: string]: ThickRawThreadInfo } = {}; for (const id in threadInfos) { const threadInfo = threadInfos[id]; if (!threadInfo.thick) { continue; } thickRawThreadInfos[id] = threadInfo; } return thickRawThreadInfos; }, ); const unreadThickThreadIDsSelector: ( state: BaseAppState<>, ) => $ReadOnlyArray = createSelector( (state: BaseAppState<>) => state.threadStore.threadInfos, (threadInfos: RawThreadInfos): $ReadOnlyArray => Object.entries(threadInfos) .filter( ([, threadInfo]) => !!threadInfo.thick && !!threadInfo.currentUser.unread, ) .map(([id]) => id), ); const unreadCount: (state: BaseAppState<>) => number = createSelector( (state: BaseAppState<>) => state.threadStore.threadInfos, (threadInfos: RawThreadInfos): number => values(threadInfos).filter( threadInfo => threadInHomeChatList(threadInfo) && threadInfo.currentUser.unread, ).length, ); const thinThreadsUnreadCountSelector: (state: BaseAppState<>) => { +[keyserverID: string]: number, } = createSelector( (state: BaseAppState<>) => state.threadStore.threadInfos, (threadInfos: RawThreadInfos): { +[keyserverID: string]: number } => { const thinThreadInfosList = values(threadInfos).filter( threadInfo => !threadInfo.thick, ); const keyserverToThreads = _groupBy(threadInfo => extractKeyserverIDFromID(threadInfo.id), )( thinThreadInfosList.filter(threadInfo => threadInHomeChatList(threadInfo), ), ); const keyserverUnreadCountPairs = Object.entries(keyserverToThreads).map( ([keyserverID, keyserverThreadInfos]) => [ keyserverID, keyserverThreadInfos.filter(threadInfo => threadInfo.currentUser.unread) .length, ], ); return Object.fromEntries(keyserverUnreadCountPairs); }, ); const unreadBackgroundCount: (state: BaseAppState<>) => number = createSelector( (state: BaseAppState<>) => state.threadStore.threadInfos, (threadInfos: RawThreadInfos): number => values(threadInfos).filter( threadInfo => threadInBackgroundChatList(threadInfo) && threadInfo.currentUser.unread, ).length, ); const baseUnreadCountSelectorForCommunity: ( communityID: string, ) => (BaseAppState<>) => number = (communityID: string) => createSelector( (state: BaseAppState<>) => state.threadStore.threadInfos, (threadInfos: RawThreadInfos): number => Object.values(threadInfos).filter( threadInfo => threadInHomeChatList(threadInfo) && threadInfo.currentUser.unread && (communityID === threadInfo.community || communityID === threadInfo.id), ).length, ); const unreadCountSelectorForCommunity: ( communityID: string, ) => (state: BaseAppState<>) => number = _memoize( baseUnreadCountSelectorForCommunity, ); const baseAncestorThreadInfos: ( threadID: string, ) => (BaseAppState<>) => $ReadOnlyArray = (threadID: string) => createSelector( (state: BaseAppState<>) => threadInfoSelector(state), (threadInfos: { +[id: string]: ThreadInfo, }): $ReadOnlyArray => { const pathComponents: ThreadInfo[] = []; let node: ?ThreadInfo = threadInfos[threadID]; while (node) { pathComponents.push(node); node = node.parentThreadID ? threadInfos[node.parentThreadID] : null; } pathComponents.reverse(); return pathComponents; }, ); const ancestorThreadInfos: ( threadID: string, ) => (state: BaseAppState<>) => $ReadOnlyArray = _memoize( baseAncestorThreadInfos, ); const baseOtherUsersButNoOtherAdmins: ( threadID: string, ) => (BaseAppState<>) => boolean = (threadID: string) => createSelector( (state: BaseAppState<>) => state.threadStore.threadInfos[threadID], relativeMemberInfoSelectorForMembersOfThread(threadID), ( threadInfo: ?RawThreadInfo, members: $ReadOnlyArray, ): boolean => { if (!threadInfo) { return false; } if (!threadHasAdminRole(threadInfo)) { return false; } let otherUsersExist = false; let otherAdminsExist = false; for (const member of members) { const role = member.role; if (role === undefined || role === null || member.isViewer) { continue; } otherUsersExist = true; if (roleIsAdminRole(threadInfo?.roles[role])) { otherAdminsExist = true; break; } } return otherUsersExist && !otherAdminsExist; }, ); const otherUsersButNoOtherAdmins: ( threadID: string, ) => (state: BaseAppState<>) => boolean = _memoize( baseOtherUsersButNoOtherAdmins, ); function mostRecentlyReadThread( messageStore: MessageStore, threadInfos: MixedRawThreadInfos, ): ?string { let mostRecent = null; for (const threadID in threadInfos) { const threadInfo = threadInfos[threadID]; if (threadInfo.currentUser.unread) { continue; } const threadMessageInfo = messageStore.threads[threadID]; if (!threadMessageInfo) { continue; } const mostRecentMessageTime = threadMessageInfo.messageIDs.length === 0 ? threadInfo.creationTime : messageStore.messages[threadMessageInfo.messageIDs[0]].time; if (mostRecent && mostRecent.time >= mostRecentMessageTime) { continue; } const topLevelThreadID = threadTypeIsSidebar(threadInfo.type) ? threadInfo.parentThreadID : threadID; mostRecent = { threadID: topLevelThreadID, time: mostRecentMessageTime }; } return mostRecent ? mostRecent.threadID : null; } const mostRecentlyReadThreadSelector: (state: BaseAppState<>) => ?string = createSelector( (state: BaseAppState<>) => state.messageStore, (state: BaseAppState<>) => state.threadStore.threadInfos, mostRecentlyReadThread, ); const threadInfoFromSourceMessageIDSelector: (state: BaseAppState<>) => { +[id: string]: ThreadInfo, } = createSelector( (state: BaseAppState<>) => state.threadStore.threadInfos, threadInfoSelector, ( rawThreadInfos: RawThreadInfos, threadInfos: { +[id: string]: ThreadInfo, }, ) => { const pendingToRealizedThreadIDs = pendingToRealizedThreadIDsSelector(rawThreadInfos); const result: { [string]: ThreadInfo } = {}; for (const realizedID of pendingToRealizedThreadIDs.values()) { const threadInfo = threadInfos[realizedID]; if (threadInfo && threadInfo.sourceMessageID) { result[threadInfo.sourceMessageID] = threadInfo; } } return result; }, ); const pendingToRealizedThreadIDsSelector: ( rawThreadInfos: RawThreadInfos, ) => $ReadOnlyMap = createSelector( (rawThreadInfos: RawThreadInfos) => rawThreadInfos, (rawThreadInfos: RawThreadInfos) => { const result = new Map(); for (const threadID in rawThreadInfos) { const rawThreadInfo = rawThreadInfos[threadID]; if ( threadIsPending(threadID) || (rawThreadInfo.parentThreadID !== genesis().id && !threadTypeIsSidebar(rawThreadInfo.type) && !threadTypeIsThick(rawThreadInfo.type)) ) { continue; } + const actualMemberIDs = rawThreadInfo.members .filter(member => member.role) .map(member => member.id); + // In this function we're generating possible pending thread IDs that // could become `rawThreadInfos`. It is possible that a thick pending // thread becomes a thin thread, so we're including it twice in a map - // for each possible pending thread ID. - const possiblePendingThreadTypes = [ - pendingThreadType(actualMemberIDs.length - 1, 'thin'), - pendingThreadType(actualMemberIDs.length - 1, 'thick'), - ]; + let possiblePendingThreadTypes; + if (threadTypeIsPersonal(rawThreadInfo.type)) { + possiblePendingThreadTypes = personalThreadTypes; + } else if (threadTypeIsPrivate(rawThreadInfo.type)) { + possiblePendingThreadTypes = privateThreadTypes; + } else if (threadTypeIsSidebar(rawThreadInfo.type)) { + possiblePendingThreadTypes = sidebarThreadTypes; + } else { + possiblePendingThreadTypes = [ + threadTypes.LOCAL, + threadTypes.COMMUNITY_SECRET_SUBTHREAD, + ]; + } + for (const type of possiblePendingThreadTypes) { const pendingThreadID = getPendingThreadID( type, actualMemberIDs, rawThreadInfo.sourceMessageID, ); const existingResult = result.get(pendingThreadID); if ( !existingResult || rawThreadInfos[existingResult].creationTime > rawThreadInfo.creationTime ) { result.set(pendingThreadID, threadID); } } } return result; }, ); const baseSavedEmojiAvatarSelectorForThread: ( threadID: string, containingThreadID: ?string, ) => (BaseAppState<>) => () => ClientAvatar = ( threadID: string, containingThreadID: ?string, ) => createSelector( (state: BaseAppState<>) => threadInfoSelector(state)[threadID], (state: BaseAppState<>) => containingThreadID ? threadInfoSelector(state)[containingThreadID] : null, (threadInfo: ThreadInfo, containingThreadInfo: ?ThreadInfo) => { return () => { let threadAvatar = getAvatarForThread(threadInfo, containingThreadInfo); if (threadAvatar.type !== 'emoji') { threadAvatar = getRandomDefaultEmojiAvatar(); } return threadAvatar; }; }, ); const savedEmojiAvatarSelectorForThread: ( threadID: string, containingThreadID: ?string, ) => (state: BaseAppState<>) => () => ClientEmojiAvatar = _memoize( baseSavedEmojiAvatarSelectorForThread, ); const baseThreadInfosSelectorForThreadType: ( threadType: ThreadType, ) => (BaseAppState<>) => $ReadOnlyArray = ( threadType: ThreadType, ) => createSelector( (state: BaseAppState<>) => threadInfoSelector(state), (threadInfos: { +[id: string]: ThreadInfo, }): $ReadOnlyArray => { const result = []; for (const threadID in threadInfos) { const threadInfo = threadInfos[threadID]; if (threadInfo.type === threadType) { result.push(threadInfo); } } return result; }, ); const threadInfosSelectorForThreadType: ( threadType: ThreadType, ) => (state: BaseAppState<>) => $ReadOnlyArray = _memoize( baseThreadInfosSelectorForThreadType, ); export { ancestorThreadInfos, threadInfoSelector, communityThreadSelector, onScreenThreadInfos, entryInfoSelector, currentDaysToEntries, childThreadInfos, containedThreadInfos, unreadCount, thinThreadsUnreadCountSelector, unreadBackgroundCount, unreadCountSelectorForCommunity, otherUsersButNoOtherAdmins, mostRecentlyReadThread, mostRecentlyReadThreadSelector, threadInfoFromSourceMessageIDSelector, pendingToRealizedThreadIDsSelector, savedEmojiAvatarSelectorForThread, threadInfosSelectorForThreadType, thickRawThreadInfosSelector, unreadThickThreadIDsSelector, }; diff --git a/lib/types/thread-types-enum.js b/lib/types/thread-types-enum.js index 30555afb4..6ead691b5 100644 --- a/lib/types/thread-types-enum.js +++ b/lib/types/thread-types-enum.js @@ -1,194 +1,197 @@ // @flow import invariant from 'invariant'; import type { TRefinement } from 'tcomb'; import { values } from '../utils/objects.js'; import { tNumEnum } from '../utils/validation-utils.js'; // Should be in sync with native/cpp/CommonCpp/NativeModules/\ // PersistentStorageUtilities/ThreadOperationsUtilities/ThreadTypeEnum.h export const thinThreadTypes = Object.freeze({ //OPEN: 0, (DEPRECATED) //CLOSED: 1, (DEPRECATED) //SECRET: 2, (DEPRECATED) // has parent, not top-level (appears under parent in inbox), and visible to // all members of parent SIDEBAR: 5, // canonical thread for each pair of users. represents the friendship // created under GENESIS. being deprecated in favor of PERSONAL GENESIS_PERSONAL: 6, // canonical thread for each single user // created under GENESIS. being deprecated in favor of PRIVATE GENESIS_PRIVATE: 7, // aka "org". no parent, top-level, has admin COMMUNITY_ROOT: 8, // like COMMUNITY_ROOT, but members aren't voiced COMMUNITY_ANNOUNCEMENT_ROOT: 9, // an open subthread. has parent, top-level (not sidebar), and visible to all // members of parent. root ancestor is a COMMUNITY_ROOT COMMUNITY_OPEN_SUBTHREAD: 3, // like COMMUNITY_SECRET_SUBTHREAD, but members aren't voiced COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD: 10, // a secret subthread. optional parent, top-level (not sidebar), visible only // to its members. root ancestor is a COMMUNITY_ROOT COMMUNITY_SECRET_SUBTHREAD: 4, // like COMMUNITY_SECRET_SUBTHREAD, but members aren't voiced COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD: 11, // like COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, but you can't leave GENESIS: 12, }); export type ThinThreadType = $Values; export const nonSidebarThickThreadTypes = Object.freeze({ // local "thick" thread (outside of community). no parent, can only have // sidebar children LOCAL: 13, // canonical thread for each pair of users. represents the friendship PERSONAL: 14, // canonical thread for each single user PRIVATE: 15, }); export type NonSidebarThickThreadType = $Values< typeof nonSidebarThickThreadTypes, >; export const sidebarThickThreadTypes = Object.freeze({ // has parent, not top-level (appears under parent in inbox), and visible to // all members of parent THICK_SIDEBAR: 16, }); export type SidebarThickThreadType = $Values; export const thickThreadTypes = Object.freeze({ ...nonSidebarThickThreadTypes, ...sidebarThickThreadTypes, }); export type ThickThreadType = | NonSidebarThickThreadType | SidebarThickThreadType; export type ThreadType = ThinThreadType | ThickThreadType; export const threadTypes = Object.freeze({ ...thinThreadTypes, ...thickThreadTypes, }); const thickThreadTypesSet = new Set(Object.values(thickThreadTypes)); export function threadTypeIsThick(threadType: ThreadType): boolean { return thickThreadTypesSet.has(threadType); } export function assertThinThreadType(threadType: number): ThinThreadType { invariant( threadType === 3 || threadType === 4 || threadType === 5 || threadType === 6 || threadType === 7 || threadType === 8 || threadType === 9 || threadType === 10 || threadType === 11 || threadType === 12, 'number is not ThinThreadType enum', ); return threadType; } export function assertThickThreadType(threadType: number): ThickThreadType { invariant( threadType === 13 || threadType === 14 || threadType === 15 || threadType === 16, 'number is not ThickThreadType enum', ); return threadType; } export const thickThreadTypeValidator: TRefinement = tNumEnum( values(thickThreadTypes), ); export function assertThreadType(threadType: number): ThreadType { invariant( threadType === 3 || threadType === 4 || threadType === 5 || threadType === 6 || threadType === 7 || threadType === 8 || threadType === 9 || threadType === 10 || threadType === 11 || threadType === 12 || threadType === 13 || threadType === 14 || threadType === 15 || threadType === 16, 'number is not ThreadType enum', ); return threadType; } export const threadTypeValidator: TRefinement = tNumEnum( values(threadTypes), ); -export const communityThreadTypes: $ReadOnlyArray = Object.freeze([ - threadTypes.COMMUNITY_ROOT, - threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT, - threadTypes.GENESIS, -]); - -export const announcementThreadTypes: $ReadOnlyArray = Object.freeze([ - threadTypes.GENESIS, - threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT, - threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD, - threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, -]); - -export const communitySubthreads: $ReadOnlyArray = Object.freeze([ - threadTypes.COMMUNITY_OPEN_SUBTHREAD, - threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD, - threadTypes.COMMUNITY_SECRET_SUBTHREAD, - threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, -]); - -export const sidebarThreadTypes: $ReadOnlyArray = Object.freeze([ +export const communityThreadTypes: $ReadOnlyArray = + Object.freeze([ + threadTypes.COMMUNITY_ROOT, + threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT, + threadTypes.GENESIS, + ]); + +export const announcementThreadTypes: $ReadOnlyArray = + Object.freeze([ + threadTypes.GENESIS, + threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT, + threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD, + threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, + ]); + +export const communitySubthreads: $ReadOnlyArray = + Object.freeze([ + threadTypes.COMMUNITY_OPEN_SUBTHREAD, + threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD, + threadTypes.COMMUNITY_SECRET_SUBTHREAD, + threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, + ]); + +export const sidebarThreadTypes: $ReadOnlyArray = Object.freeze([ threadTypes.SIDEBAR, threadTypes.THICK_SIDEBAR, ]); -export const personalThreadTypes: $ReadOnlyArray = Object.freeze([ +export const personalThreadTypes: $ReadOnlyArray = Object.freeze([ threadTypes.PERSONAL, threadTypes.GENESIS_PERSONAL, ]); -export const privateThreadTypes: $ReadOnlyArray = Object.freeze([ +export const privateThreadTypes: $ReadOnlyArray = Object.freeze([ threadTypes.PRIVATE, threadTypes.GENESIS_PRIVATE, ]); export function threadTypeIsCommunityRoot(threadType: ThreadType): boolean { return communityThreadTypes.includes(threadType); } export function threadTypeIsAnnouncementThread( threadType: ThreadType, ): boolean { return announcementThreadTypes.includes(threadType); } export function threadTypeIsSidebar(threadType: ThreadType): boolean { return sidebarThreadTypes.includes(threadType); } export function threadTypeIsPersonal(threadType: ThreadType): boolean { return personalThreadTypes.includes(threadType); } export function threadTypeIsPrivate(threadType: ThreadType): boolean { return privateThreadTypes.includes(threadType); }