Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3332670
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
74 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/keyserver/src/fetchers/thread-fetchers.js b/keyserver/src/fetchers/thread-fetchers.js
index 83bdf3e32..dbb8091d0 100644
--- a/keyserver/src/fetchers/thread-fetchers.js
+++ b/keyserver/src/fetchers/thread-fetchers.js
@@ -1,476 +1,490 @@
// @flow
import invariant from 'invariant';
import genesis from 'lib/facts/genesis.js';
import { specialRoles } from 'lib/permissions/special-roles.js';
import { getAllThreadPermissions } from 'lib/permissions/thread-permissions.js';
import {
rawThreadInfoFromServerThreadInfo,
getContainingThreadID,
getCommunity,
} from 'lib/shared/thread-utils.js';
import { hasMinCodeVersion } from 'lib/shared/version-utils.js';
import type { AvatarDBContent, ClientAvatar } from 'lib/types/avatar-types.js';
import type { RawMessageInfo, MessageInfo } from 'lib/types/message-types.js';
import type { ThinRawThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
import { threadTypes, type ThreadType } from 'lib/types/thread-types-enum.js';
import {
type ServerThreadInfo,
type ServerLegacyRoleInfo,
type LegacyThinRawThreadInfo,
} from 'lib/types/thread-types.js';
import { ServerError } from 'lib/utils/errors.js';
import { getUploadURL, makeUploadURI } from './upload-fetchers.js';
import { dbQuery, SQL, mergeAndConditions } from '../database/database.js';
import type { SQLStatementType } from '../database/types.js';
import type { Viewer } from '../session/viewer.js';
type FetchThreadInfosFilter = Partial<{
+accessibleToUserID: string,
+threadID: string,
+threadIDs: $ReadOnlySet<string>,
+parentThreadID: string,
+sourceMessageID: string,
}>;
function constructWhereClause(
filter: FetchThreadInfosFilter,
): SQLStatementType {
const fromTable = filter.accessibleToUserID ? 'memberships' : 'threads';
const conditions = [];
if (filter.accessibleToUserID) {
conditions.push(
SQL`mm.user = ${filter.accessibleToUserID} AND mm.role > -1`,
);
}
if (filter.threadID && fromTable === 'memberships') {
conditions.push(SQL`mm.thread = ${filter.threadID}`);
} else if (filter.threadID) {
conditions.push(SQL`t.id = ${filter.threadID}`);
}
if (filter.threadIDs && fromTable === 'memberships') {
conditions.push(SQL`mm.thread IN (${[...filter.threadIDs]})`);
} else if (filter.threadIDs) {
conditions.push(SQL`t.id IN (${[...filter.threadIDs]})`);
}
if (filter.parentThreadID) {
conditions.push(SQL`t.parent_thread_id = ${filter.parentThreadID}`);
}
if (filter.sourceMessageID) {
conditions.push(SQL`t.source_message = ${filter.sourceMessageID}`);
}
if (conditions.length === 0) {
return SQL``;
}
const clause = mergeAndConditions(conditions);
return SQL`WHERE `.append(clause);
}
type FetchServerThreadInfosResult = {
+threadInfos: { +[id: string]: ServerThreadInfo },
};
async function fetchServerThreadInfos(
filter?: FetchThreadInfosFilter,
): Promise<FetchServerThreadInfosResult> {
if (filter?.threadIDs?.size === 0) {
return { threadInfos: {} };
}
let primaryFetchClause;
if (filter?.accessibleToUserID) {
primaryFetchClause = SQL`
FROM memberships mm
LEFT JOIN threads t ON t.id = mm.thread
`;
} else {
primaryFetchClause = SQL`
FROM threads t
`;
}
const whereClause = filter ? constructWhereClause(filter) : '';
const rolesQuery = SQL`
SELECT t.id, r.id AS role, r.name, r.permissions, r.special_role,
r.special_role = ${specialRoles.DEFAULT_ROLE} AS is_default
`
.append(primaryFetchClause)
.append(
SQL`
LEFT JOIN roles r ON r.thread = t.id
`,
)
.append(whereClause);
const threadsQuery = SQL`
SELECT t.id, t.name, t.parent_thread_id, t.containing_thread_id,
t.community, t.depth, t.color, t.description, t.type, t.creation_time,
t.source_message, t.replies_count, t.avatar, t.pinned_count, m.user,
m.role, m.permissions, m.subscription,
m.last_read_message < m.last_message AS unread, m.sender,
up.id AS upload_id, up.secret AS upload_secret, up.extra AS upload_extra
`
.append(primaryFetchClause)
.append(
SQL`
LEFT JOIN memberships m ON m.thread = t.id AND m.role >= 0
LEFT JOIN uploads up ON up.container = t.id
`,
)
.append(whereClause)
.append(SQL` ORDER BY m.user ASC`);
const [[threadsResult], [rolesResult]] = await Promise.all([
dbQuery(threadsQuery),
dbQuery(rolesQuery),
]);
const threadInfos = {};
for (const threadsRow of threadsResult) {
const threadID = threadsRow.id.toString();
if (!threadInfos[threadID]) {
threadInfos[threadID] = {
id: threadID,
type: threadsRow.type,
name: threadsRow.name ? threadsRow.name : '',
description: threadsRow.description ? threadsRow.description : '',
color: threadsRow.color,
creationTime: threadsRow.creation_time,
parentThreadID: threadsRow.parent_thread_id
? threadsRow.parent_thread_id.toString()
: null,
containingThreadID: threadsRow.containing_thread_id
? threadsRow.containing_thread_id.toString()
: null,
depth: threadsRow.depth,
community: threadsRow.community
? threadsRow.community.toString()
: null,
members: [],
roles: {},
repliesCount: threadsRow.replies_count,
pinnedCount: threadsRow.pinned_count,
};
if (threadsRow.avatar) {
const avatar: AvatarDBContent = JSON.parse(threadsRow.avatar);
let clientAvatar: ?ClientAvatar;
if (
avatar &&
avatar.type !== 'image' &&
avatar.type !== 'encrypted_image'
) {
clientAvatar = avatar;
} else if (
avatar &&
(avatar.type === 'image' || avatar.type === 'encrypted_image') &&
threadsRow.upload_id &&
threadsRow.upload_secret
) {
const uploadID = threadsRow.upload_id.toString();
invariant(
uploadID === avatar.uploadID,
`uploadID of upload should match uploadID of image avatar`,
);
if (avatar.type === 'encrypted_image' && threadsRow.upload_extra) {
const uploadExtra = JSON.parse(threadsRow.upload_extra);
clientAvatar = {
type: 'encrypted_image',
blobURI: makeUploadURI(
uploadExtra.blobHash,
uploadID,
threadsRow.upload_secret,
),
encryptionKey: uploadExtra.encryptionKey,
thumbHash: uploadExtra.thumbHash,
};
} else {
clientAvatar = {
type: 'image',
uri: getUploadURL(uploadID, threadsRow.upload_secret),
};
}
}
threadInfos[threadID] = {
...threadInfos[threadID],
avatar: clientAvatar,
};
}
}
const sourceMessageID = threadsRow.source_message?.toString();
if (sourceMessageID) {
threadInfos[threadID].sourceMessageID = sourceMessageID;
}
if (threadsRow.user) {
const userID = threadsRow.user.toString();
const allPermissions = getAllThreadPermissions(
JSON.parse(threadsRow.permissions),
threadID,
);
threadInfos[threadID].members.push({
id: userID,
permissions: allPermissions,
role: threadsRow.role ? threadsRow.role.toString() : null,
subscription: JSON.parse(threadsRow.subscription),
unread: threadsRow.role ? !!threadsRow.unread : null,
isSender: !!threadsRow.sender,
});
}
}
for (const rolesRow of rolesResult) {
const threadID = rolesRow.id.toString();
if (!rolesRow.role) {
continue;
}
const role = rolesRow.role.toString();
if (!threadInfos[threadID].roles[role]) {
const roleInfo: ServerLegacyRoleInfo = {
id: role,
name: rolesRow.name,
permissions: JSON.parse(rolesRow.permissions),
isDefault: Boolean(rolesRow.is_default),
specialRole: rolesRow.special_role,
};
threadInfos[threadID].roles[role] = roleInfo;
}
}
return { threadInfos };
}
export type FetchThreadInfosResult = {
+threadInfos: {
+[id: string]: LegacyThinRawThreadInfo | ThinRawThreadInfo,
},
};
async function fetchThreadInfos(
viewer: Viewer,
inputFilter?: FetchThreadInfosFilter,
): Promise<FetchThreadInfosResult> {
const filter = {
accessibleToUserID: viewer.id,
...inputFilter,
};
const serverResult = await fetchServerThreadInfos(filter);
return rawThreadInfosFromServerThreadInfos(viewer, serverResult);
}
+async function fetchPrivilegedThreadInfos(
+ viewer: Viewer,
+ inputFilter?: FetchThreadInfosFilter,
+): Promise<FetchThreadInfosResult> {
+ const serverResult = await fetchServerThreadInfos(inputFilter);
+ return rawThreadInfosFromServerThreadInfos(viewer, serverResult, {
+ dontFilterMissingKnowOf: true,
+ });
+}
+
function rawThreadInfosFromServerThreadInfos(
viewer: Viewer,
serverResult: FetchServerThreadInfosResult,
+ options?: { +dontFilterMissingKnowOf?: boolean },
): FetchThreadInfosResult {
const viewerID = viewer.id;
+ const dontFilterMissingKnowOf = options?.dontFilterMissingKnowOf ?? false;
const codeVersionBelow209 = !hasMinCodeVersion(viewer.platformDetails, {
native: 209,
});
const codeVersionBelow213 = !hasMinCodeVersion(viewer.platformDetails, {
native: 213,
});
const codeVersionBelow221 = !hasMinCodeVersion(viewer.platformDetails, {
native: 221,
});
const codeVersionBelow283 = !hasMinCodeVersion(viewer.platformDetails, {
native: 285,
});
const minimallyEncodedPermissionsSupported = hasMinCodeVersion(
viewer.platformDetails,
{ native: 301, web: 56 },
);
const specialRoleFieldSupported = hasMinCodeVersion(viewer.platformDetails, {
native: 336,
web: 79,
});
const addingUsersToCommunityRootSupported = !hasMinCodeVersion(
viewer.platformDetails,
{
native: 355,
web: 88,
},
);
const manageFarcasterChannelTagsPermissionUnsupported = !hasMinCodeVersion(
viewer.platformDetails,
{
native: 355,
web: 88,
},
);
const stripMemberPermissions = hasMinCodeVersion(viewer.platformDetails, {
native: 379,
web: 130,
});
const canDisplayFarcasterThreadAvatars = hasMinCodeVersion(
viewer.platformDetails,
{
native: 429,
web: 136,
},
);
const threadInfos: {
[string]: LegacyThinRawThreadInfo | ThinRawThreadInfo,
} = {};
for (const threadID in serverResult.threadInfos) {
const serverThreadInfo = serverResult.threadInfos[threadID];
const threadInfo = rawThreadInfoFromServerThreadInfo(
serverThreadInfo,
viewerID,
{
filterThreadEditAvatarPermission: codeVersionBelow213,
excludePinInfo: codeVersionBelow209,
filterManageInviteLinksPermission: codeVersionBelow221,
filterVoicedInAnnouncementChannelsPermission: codeVersionBelow283,
minimallyEncodePermissions: minimallyEncodedPermissionsSupported,
includeSpecialRoleFieldInRoles: specialRoleFieldSupported,
allowAddingUsersToCommunityRoot: addingUsersToCommunityRootSupported,
filterManageFarcasterChannelTagsPermission:
manageFarcasterChannelTagsPermissionUnsupported,
stripMemberPermissions: stripMemberPermissions,
canDisplayFarcasterThreadAvatars,
+ dontFilterMissingKnowOf,
},
);
if (threadInfo) {
threadInfos[threadID] = threadInfo;
}
}
return { threadInfos };
}
async function verifyThreadIDs(
threadIDs: $ReadOnlyArray<string>,
): Promise<$ReadOnlyArray<string>> {
if (threadIDs.length === 0) {
return [];
}
const query = SQL`SELECT id FROM threads WHERE id IN (${threadIDs})`;
const [result] = await dbQuery(query);
const verified = [];
for (const row of result) {
verified.push(row.id.toString());
}
return verified;
}
async function verifyThreadID(threadID: string): Promise<boolean> {
const result = await verifyThreadIDs([threadID]);
return result.length !== 0;
}
type ThreadAncestry = {
+containingThreadID: ?string,
+community: ?string,
+depth: number,
};
async function determineThreadAncestry(
parentThreadID: ?string,
threadType: ThreadType,
): Promise<ThreadAncestry> {
if (!parentThreadID) {
return { containingThreadID: null, community: null, depth: 0 };
}
const parentThreadInfos = await fetchServerThreadInfos({
threadID: parentThreadID,
});
const parentThreadInfo = parentThreadInfos.threadInfos[parentThreadID];
if (!parentThreadInfo) {
throw new ServerError('invalid_parameters');
}
const containingThreadID = getContainingThreadID(
parentThreadInfo,
threadType,
);
const community = getCommunity(parentThreadInfo);
const depth = parentThreadInfo.depth + 1;
return { containingThreadID, community, depth };
}
function determineThreadAncestryForPossibleMemberResolution(
parentThreadID: ?string,
containingThreadID: ?string,
): ?string {
let resolvedContainingThreadID = containingThreadID;
if (resolvedContainingThreadID === genesis().id) {
resolvedContainingThreadID =
parentThreadID === genesis().id ? null : parentThreadID;
}
return resolvedContainingThreadID;
}
function personalThreadQuery(
firstMemberID: string,
secondMemberID: string,
): SQLStatementType {
return SQL`
SELECT t.id
FROM threads t
INNER JOIN memberships m1
ON m1.thread = t.id AND m1.user = ${firstMemberID}
INNER JOIN memberships m2
ON m2.thread = t.id AND m2.user = ${secondMemberID}
WHERE t.type = ${threadTypes.GENESIS_PERSONAL}
AND m1.role > 0
AND m2.role > 0
`;
}
async function fetchPersonalThreadID(
viewerID: string,
otherMemberID: string,
): Promise<?string> {
const query = personalThreadQuery(viewerID, otherMemberID);
const [threads] = await dbQuery(query);
return threads[0]?.id.toString();
}
async function serverThreadInfoFromMessageInfo(
message: RawMessageInfo | MessageInfo,
): Promise<?ServerThreadInfo> {
const threadID = message.threadID;
const threads = await fetchServerThreadInfos({ threadID });
return threads.threadInfos[threadID];
}
async function fetchContainedThreadIDs(
parentThreadID: string,
): Promise<Array<string>> {
const query = SQL`
WITH RECURSIVE thread_tree AS (
SELECT id, containing_thread_id
FROM threads
WHERE id = ${parentThreadID}
UNION ALL
SELECT t.id, t.containing_thread_id
FROM threads t
JOIN thread_tree tt ON t.containing_thread_id = tt.id
)
SELECT id FROM thread_tree
`;
const [result] = await dbQuery(query);
return result.map(row => row.id.toString());
}
export {
fetchServerThreadInfos,
+ fetchPrivilegedThreadInfos,
fetchThreadInfos,
rawThreadInfosFromServerThreadInfos,
verifyThreadIDs,
verifyThreadID,
determineThreadAncestry,
determineThreadAncestryForPossibleMemberResolution,
personalThreadQuery,
fetchPersonalThreadID,
serverThreadInfoFromMessageInfo,
fetchContainedThreadIDs,
};
diff --git a/lib/shared/thread-utils.js b/lib/shared/thread-utils.js
index 335e8da44..6d67418f1 100644
--- a/lib/shared/thread-utils.js
+++ b/lib/shared/thread-utils.js
@@ -1,1968 +1,1973 @@
// @flow
import invariant from 'invariant';
import _find from 'lodash/fp/find.js';
import _keyBy from 'lodash/fp/keyBy.js';
import _mapValues from 'lodash/fp/mapValues.js';
import _omit from 'lodash/fp/omit.js';
import _omitBy from 'lodash/fp/omitBy.js';
import * as React from 'react';
import { getUserAvatarForThread } from './avatar-utils.js';
import { generatePendingThreadColor } from './color-utils.js';
import { extractUserMentionsFromText } from './mention-utils.js';
import { relationshipBlockedInEitherDirection } from './relationship-utils.js';
import type { SidebarItem } from './sidebar-item-utils.js';
import ashoat from '../facts/ashoat.js';
import genesis from '../facts/genesis.js';
import { useLoggedInUserInfo } from '../hooks/account-hooks.js';
import { type UserSearchResult } from '../hooks/thread-search-hooks.js';
import { useUsersSupportThickThreads } from '../hooks/user-identities-hooks.js';
import { extractKeyserverIDFromIDOptional } from '../keyserver-conn/keyserver-call-utils.js';
import {
hasPermission,
permissionsToBitmaskHex,
threadPermissionsFromBitmaskHex,
} from '../permissions/minimally-encoded-thread-permissions.js';
import { specialRoles } from '../permissions/special-roles.js';
import type { SpecialRole } from '../permissions/special-roles.js';
import {
permissionLookup,
getAllThreadPermissions,
makePermissionsBlob,
} from '../permissions/thread-permissions.js';
import type { ChatThreadItem } from '../selectors/chat-selectors.js';
import {
threadInfoSelector,
pendingToRealizedThreadIDsSelector,
threadInfosSelectorForThreadType,
onScreenThreadInfos,
} from '../selectors/thread-selectors.js';
import {
getRelativeMemberInfos,
usersWithPersonalThreadSelector,
} from '../selectors/user-selectors.js';
import type { AuxUserInfos } from '../types/aux-user-types.js';
import type { RawDeviceList } from '../types/identity-service-types.js';
import type {
RelativeMemberInfo,
RawThreadInfo,
MemberInfoWithPermissions,
RoleInfo,
ThreadInfo,
MinimallyEncodedThickMemberInfo,
ThinRawThreadInfo,
} from '../types/minimally-encoded-thread-permissions-types.js';
import {
decodeMinimallyEncodedRoleInfo,
minimallyEncodeMemberInfo,
minimallyEncodeRawThreadInfoWithMemberPermissions,
minimallyEncodeRoleInfo,
minimallyEncodeThreadCurrentUserInfo,
} from '../types/minimally-encoded-thread-permissions-types.js';
import { userRelationshipStatus } from '../types/relationship-types.js';
import { defaultThreadSubscription } from '../types/subscription-types.js';
import {
threadPermissionPropagationPrefixes,
threadPermissions,
type ThreadPermission,
type ThreadPermissionsInfo,
type ThreadRolePermissionsBlob,
type UserSurfacedPermission,
threadPermissionFilterPrefixes,
threadPermissionsDisabledByBlock,
type ThreadPermissionNotAffectedByBlock,
} from '../types/thread-permission-types.js';
import {
type ThreadType,
threadTypes,
threadTypeIsCommunityRoot,
assertThreadType,
threadTypeIsThick,
assertThinThreadType,
assertThickThreadType,
threadTypeIsSidebar,
threadTypeIsPrivate,
threadTypeIsPersonal,
type ThinThreadType,
} from '../types/thread-types-enum.js';
import type {
LegacyRawThreadInfo,
ClientLegacyRoleInfo,
ServerThreadInfo,
ThickMemberInfo,
UserProfileThreadInfo,
MixedRawThreadInfos,
LegacyThinRawThreadInfo,
ThreadTimestamps,
} from '../types/thread-types.js';
import { updateTypes } from '../types/update-types-enum.js';
import { type ClientUpdateInfo } from '../types/update-types.js';
import type {
UserInfos,
AccountUserInfo,
LoggedInUserInfo,
UserInfo,
} from '../types/user-types.js';
import {
ET,
type ThreadEntity,
type UserEntity,
} from '../utils/entity-text.js';
import {
stripMemberPermissionsFromRawThreadInfo,
type ThinRawThreadInfoWithPermissions,
} from '../utils/member-info-utils.js';
import { entries, values } from '../utils/objects.js';
import { useSelector } from '../utils/redux-utils.js';
import { userSurfacedPermissionsFromRolePermissions } from '../utils/role-utils.js';
import { firstLine } from '../utils/string-utils.js';
import {
pendingThreadIDRegex,
pendingThickSidebarURLPrefix,
pendingSidebarURLPrefix,
} from '../utils/validation-utils.js';
function threadHasPermission(
threadInfo: ?(ThreadInfo | LegacyRawThreadInfo | RawThreadInfo),
permission: ThreadPermissionNotAffectedByBlock,
): boolean {
if (!threadInfo) {
return false;
}
invariant(
!permissionsDisabledByBlock.has(permission) || threadInfo?.uiName,
`${permission} can be disabled by a block, but threadHasPermission can't ` +
'check for a block on RawThreadInfo. Please pass in ThreadInfo instead!',
);
if (threadInfo.minimallyEncoded) {
return hasPermission(threadInfo.currentUser.permissions, permission);
}
return permissionLookup(threadInfo.currentUser.permissions, permission);
}
type CommunityRootMembersToRoleType = {
+[threadID: ?string]: {
+[memberID: string]: ?RoleInfo,
},
};
function useCommunityRootMembersToRole(
threadInfos: $ReadOnlyArray<RawThreadInfo | ThreadInfo>,
): CommunityRootMembersToRoleType {
const communityRootMembersToRole = React.useMemo(() => {
const communityThreadInfos = threadInfos.filter(threadInfo =>
threadTypeIsCommunityRoot(threadInfo.type),
);
if (communityThreadInfos.length === 0) {
return {};
}
const communityRoots = _keyBy('id')(communityThreadInfos);
return _mapValues((threadInfo: ThreadInfo) => {
const keyedMembers = _keyBy('id')(threadInfo.members);
const keyedMembersToRole = _mapValues(
(member: MemberInfoWithPermissions | RelativeMemberInfo) => {
return member.role ? threadInfo.roles[member.role] : null;
},
)(keyedMembers);
return keyedMembersToRole;
})(communityRoots);
}, [threadInfos]);
return communityRootMembersToRole;
}
// This function returns true for all thick threads, as well as all channels
// inside GENESIS. Channels inside GENESIS were used in place of thick threads
// before thick threads were launched, and as such we mirror "freezing" behavior
// between them and thick threads. "Freezing" a thread can occur when a user
// blocks another user, and those two users are the only members of a given
// chat. Note that we exclude the GENESIS community root here, as the root
// itself has never been used in place of thick threads. Also note that
// grandchild channels of GENESIS get this behavior too, even though we don't
// currently support channels inside thick threads.
function threadIsThickOrChannelInsideGenesis(threadInfo: ThreadInfo): boolean {
if (threadTypeIsThick(threadInfo.type)) {
return true;
}
if (getCommunity(threadInfo) !== genesis().id) {
return false;
}
return threadInfo.id !== genesis().id;
}
function useThreadsWithPermission(
threadInfos: $ReadOnlyArray<ThreadInfo>,
permission: ThreadPermission,
): $ReadOnlyArray<ThreadInfo> {
const loggedInUserInfo = useLoggedInUserInfo();
const userInfos = useSelector(state => state.userStore.userInfos);
return React.useMemo(() => {
return threadInfos.filter((threadInfo: ThreadInfo) => {
const isGroupChat = threadIsThickOrChannelInsideGenesis(threadInfo);
if (!isGroupChat || !loggedInUserInfo) {
return hasPermission(threadInfo.currentUser.permissions, permission);
}
const threadFrozen = threadIsWithBlockedUserOnlyWithoutAdminRoleCheck(
threadInfo,
loggedInUserInfo.id,
userInfos,
false,
);
const permissions = threadFrozen
? filterOutDisabledPermissions(threadInfo.currentUser.permissions)
: threadInfo.currentUser.permissions;
return hasPermission(permissions, permission);
});
}, [threadInfos, loggedInUserInfo, userInfos, permission]);
}
function useThreadHasPermission(
threadInfo: ?ThreadInfo,
permission: ThreadPermission,
): boolean {
const threadInfos = React.useMemo(
() => (threadInfo ? [threadInfo] : []),
[threadInfo],
);
const threads = useThreadsWithPermission(threadInfos, permission);
return threads.length === 1;
}
function viewerIsMember(
threadInfo: ?(ThreadInfo | LegacyRawThreadInfo | RawThreadInfo),
): boolean {
return !!(
threadInfo &&
threadInfo.currentUser.role !== null &&
threadInfo.currentUser.role !== undefined
);
}
function isMemberActive(
memberInfo: MemberInfoWithPermissions | MinimallyEncodedThickMemberInfo,
): boolean {
const role = memberInfo.role;
return role !== null && role !== undefined;
}
function threadIsInHome(threadInfo: ?(RawThreadInfo | ThreadInfo)): boolean {
return !!(threadInfo && threadInfo.currentUser.subscription.home);
}
// Can have messages
function threadInChatList(
threadInfo: ?(LegacyRawThreadInfo | RawThreadInfo | ThreadInfo),
): boolean {
return (
viewerIsMember(threadInfo) &&
threadHasPermission(threadInfo, threadPermissions.VISIBLE)
);
}
function useIsThreadInChatList(threadInfo: ?ThreadInfo): boolean {
const threadIsVisible = useThreadHasPermission(
threadInfo,
threadPermissions.VISIBLE,
);
return viewerIsMember(threadInfo) && threadIsVisible;
}
function useThreadsInChatList(
threadInfos: $ReadOnlyArray<ThreadInfo>,
): $ReadOnlyArray<ThreadInfo> {
const visibleThreads = useThreadsWithPermission(
threadInfos,
threadPermissions.VISIBLE,
);
return React.useMemo(
() => visibleThreads.filter(viewerIsMember),
[visibleThreads],
);
}
function threadIsTopLevel(threadInfo: ?(RawThreadInfo | ThreadInfo)): boolean {
return threadInChatList(threadInfo) && threadIsChannel(threadInfo);
}
function threadIsChannel(threadInfo: ?(RawThreadInfo | ThreadInfo)): boolean {
return !!(threadInfo && !threadTypeIsSidebar(threadInfo.type));
}
function threadIsSidebar(threadInfo: ?(RawThreadInfo | ThreadInfo)): boolean {
return !!(threadInfo && threadTypeIsSidebar(threadInfo.type));
}
function threadInBackgroundChatList(
threadInfo: ?(RawThreadInfo | ThreadInfo),
): boolean {
return threadInChatList(threadInfo) && !threadIsInHome(threadInfo);
}
function threadInHomeChatList(
threadInfo: ?(RawThreadInfo | ThreadInfo),
): boolean {
return threadInChatList(threadInfo) && threadIsInHome(threadInfo);
}
// Can have Calendar entries,
// does appear as a top-level entity in the thread list
function threadInFilterList(
threadInfo: ?(LegacyRawThreadInfo | RawThreadInfo | ThreadInfo),
): boolean {
return (
threadInChatList(threadInfo) &&
!!threadInfo &&
!threadTypeIsSidebar(threadInfo.type)
);
}
function userIsMember(
threadInfo: ?(RawThreadInfo | ThreadInfo),
userID: string,
): boolean {
if (!threadInfo) {
return false;
}
if (threadInfo.id === genesis().id) {
return true;
}
return threadInfo.members.some(member => member.id === userID && member.role);
}
function threadActualMembers(
memberInfos: $ReadOnlyArray<MemberInfoWithPermissions | RelativeMemberInfo>,
): $ReadOnlyArray<string> {
return memberInfos
.filter(memberInfo => memberInfo.role)
.map(memberInfo => memberInfo.id);
}
type MemberIDAndRole = {
+id: string,
+role: ?string,
...
};
function threadOtherMembers<T: MemberIDAndRole>(
memberInfos: $ReadOnlyArray<T>,
viewerID: ?string,
): $ReadOnlyArray<T> {
return memberInfos.filter(
memberInfo => memberInfo.role && memberInfo.id !== viewerID,
);
}
function threadMembersWithoutAddedAdmin<
T: LegacyRawThreadInfo | RawThreadInfo | ThreadInfo,
>(threadInfo: T): $PropertyType<T, 'members'> {
if (threadInfo.community !== genesis().id) {
return threadInfo.members;
}
const adminID = extractKeyserverIDFromIDOptional(threadInfo.id);
return threadInfo.members.filter(
member => member.id !== adminID || member.role,
);
}
function threadIsGroupChat(threadInfo: ThreadInfo): boolean {
return threadInfo.members.length > 2;
}
function threadOrParentThreadIsGroupChat(
threadInfo: LegacyRawThreadInfo | RawThreadInfo | ThreadInfo,
) {
return threadMembersWithoutAddedAdmin(threadInfo).length > 2;
}
function threadIsPending(threadID: ?string): boolean {
return !!threadID?.startsWith('pending');
}
function threadIsPendingSidebar(threadID: ?string): boolean {
return (
!!threadID?.startsWith(`pending/${pendingSidebarURLPrefix}/`) ||
!!threadID?.startsWith(`pending/${pendingThickSidebarURLPrefix}`)
);
}
function getSingleOtherUser(
threadInfo: LegacyRawThreadInfo | RawThreadInfo | ThreadInfo,
viewerID: ?string,
): ?string {
if (!viewerID) {
return undefined;
}
const otherMembers = threadOtherMembers(threadInfo.members, viewerID);
if (otherMembers.length !== 1) {
return undefined;
}
return otherMembers[0].id;
}
function getPendingThreadID(
threadType: ThreadType,
memberIDs: $ReadOnlyArray<string>,
sourceMessageID: ?string,
): string {
let pendingThreadKey;
if (sourceMessageID && threadTypeIsThick(threadType)) {
pendingThreadKey = `${pendingThickSidebarURLPrefix}/${sourceMessageID}`;
} else if (sourceMessageID) {
pendingThreadKey = `${pendingSidebarURLPrefix}/${sourceMessageID}`;
} else {
pendingThreadKey = [...memberIDs].sort().join('+');
}
const pendingThreadTypeString = sourceMessageID ? '' : `type${threadType}/`;
return `pending/${pendingThreadTypeString}${pendingThreadKey}`;
}
type PendingThreadIDContents = {
+threadType: ThreadType,
+memberIDs: $ReadOnlyArray<string>,
+sourceMessageID: ?string,
};
function parsePendingThreadID(
pendingThreadID: string,
): ?PendingThreadIDContents {
const pendingRegex = new RegExp(`^${pendingThreadIDRegex}$`);
const pendingThreadIDMatches = pendingRegex.exec(pendingThreadID);
if (!pendingThreadIDMatches) {
return null;
}
const [threadTypeString, threadKey] = pendingThreadIDMatches[1].split('/');
let threadType;
if (threadTypeString === pendingThickSidebarURLPrefix) {
threadType = threadTypes.THICK_SIDEBAR;
} else if (threadTypeString === pendingSidebarURLPrefix) {
threadType = threadTypes.SIDEBAR;
} else {
threadType = assertThreadType(Number(threadTypeString.replace('type', '')));
}
const threadTypeStringIsSidebar =
threadTypeString === pendingSidebarURLPrefix ||
threadTypeString === pendingThickSidebarURLPrefix;
const memberIDs = threadTypeStringIsSidebar ? [] : threadKey.split('+');
const sourceMessageID = threadTypeStringIsSidebar ? threadKey : null;
return {
threadType,
memberIDs,
sourceMessageID,
};
}
type UserIDAndUsername = {
+id: string,
+username: ?string,
...
};
type CreatePendingThreadArgs = {
+viewerID: string,
+threadType: ThreadType,
+members: $ReadOnlyArray<UserIDAndUsername>,
+parentThreadInfo?: ?ThreadInfo,
+threadColor?: ?string,
+name?: ?string,
+sourceMessageID?: string,
};
function createPendingThread({
viewerID,
threadType,
members,
parentThreadInfo,
threadColor,
name,
sourceMessageID,
}: CreatePendingThreadArgs): ThreadInfo {
const now = Date.now();
if (!members.some(member => member.id === viewerID)) {
throw new Error(
'createPendingThread should be called with the viewer as a member',
);
}
const memberIDs = members.map(member => member.id);
const threadID = getPendingThreadID(threadType, memberIDs, sourceMessageID);
const permissions: ThreadRolePermissionsBlob = {
[threadPermissions.KNOW_OF]: true,
[threadPermissions.VISIBLE]: true,
[threadPermissions.VOICED]: true,
};
const membershipPermissions = getAllThreadPermissions(
makePermissionsBlob(permissions, null, threadID, threadType),
threadID,
);
const role: RoleInfo = {
...minimallyEncodeRoleInfo({
id: `${threadID}/role`,
name: 'Members',
permissions,
isDefault: true,
}),
specialRole: specialRoles.DEFAULT_ROLE,
};
let rawThreadInfo: RawThreadInfo;
if (threadTypeIsThick(threadType)) {
const thickThreadType = assertThickThreadType(threadType);
rawThreadInfo = {
minimallyEncoded: true,
thick: true,
id: threadID,
type: thickThreadType,
name: name ?? null,
description: null,
color: threadColor ?? generatePendingThreadColor(memberIDs),
creationTime: now,
parentThreadID: parentThreadInfo?.id ?? null,
containingThreadID: getContainingThreadID(
parentThreadInfo,
thickThreadType,
),
members: members.map(member =>
minimallyEncodeMemberInfo<ThickMemberInfo>({
id: member.id,
role: role.id,
permissions: membershipPermissions,
isSender: false,
subscription: defaultThreadSubscription,
}),
),
roles: {
[role.id]: role,
},
currentUser: minimallyEncodeThreadCurrentUserInfo({
role: role.id,
permissions: membershipPermissions,
subscription: defaultThreadSubscription,
unread: false,
}),
repliesCount: 0,
sourceMessageID,
pinnedCount: 0,
timestamps: createThreadTimestamps(now, memberIDs),
};
} else {
const thinThreadType = assertThinThreadType(threadType);
rawThreadInfo = {
minimallyEncoded: true,
id: threadID,
type: thinThreadType,
name: name ?? null,
description: null,
color: threadColor ?? generatePendingThreadColor(memberIDs),
creationTime: now,
parentThreadID: parentThreadInfo?.id ?? null,
containingThreadID: getContainingThreadID(
parentThreadInfo,
thinThreadType,
),
community: getCommunity(parentThreadInfo),
members: members.map(member => ({
id: member.id,
role: role.id,
minimallyEncoded: true,
isSender: false,
})),
roles: {
[role.id]: role,
},
currentUser: minimallyEncodeThreadCurrentUserInfo({
role: role.id,
permissions: membershipPermissions,
subscription: defaultThreadSubscription,
unread: false,
}),
repliesCount: 0,
sourceMessageID,
pinnedCount: 0,
};
}
const userInfos: { [string]: UserInfo } = {};
for (const member of members) {
const { id, username } = member;
userInfos[id] = { id, username };
}
return threadInfoFromRawThreadInfo(rawThreadInfo, viewerID, userInfos);
}
type PendingPersonalThread = {
+threadInfo: ThreadInfo,
+pendingPersonalThreadUserInfo: UserInfo,
};
function createPendingPersonalOrPrivateThread(
loggedInUserInfo: LoggedInUserInfo,
userID: string,
username: ?string,
supportThickThreads: boolean,
): PendingPersonalThread {
const pendingPersonalThreadUserInfo = {
id: userID,
username: username,
};
const members: Array<UserIDAndUsername> = [loggedInUserInfo];
let threadType;
if (loggedInUserInfo.id === userID) {
threadType = supportThickThreads
? threadTypes.PRIVATE
: threadTypes.GENESIS_PRIVATE;
} else {
threadType = supportThickThreads
? threadTypes.PERSONAL
: threadTypes.GENESIS_PERSONAL;
members.push(pendingPersonalThreadUserInfo);
}
const threadInfo = createPendingThread({
viewerID: loggedInUserInfo.id,
threadType,
members,
});
return { threadInfo, pendingPersonalThreadUserInfo };
}
function createPendingThreadItem(
loggedInUserInfo: LoggedInUserInfo,
user: UserIDAndUsername,
supportThickThreads: boolean,
): ChatThreadItem {
const { threadInfo, pendingPersonalThreadUserInfo } =
createPendingPersonalOrPrivateThread(
loggedInUserInfo,
user.id,
user.username,
supportThickThreads,
);
return {
type: 'chatThreadItem',
threadInfo,
mostRecentNonLocalMessage: null,
lastUpdatedTime: threadInfo.creationTime,
lastUpdatedTimeIncludingSidebars: threadInfo.creationTime,
sidebars: [],
pendingPersonalThreadUserInfo,
};
}
// Returns map from lowercase username to AccountUserInfo
function memberLowercaseUsernameMap(
members: $ReadOnlyArray<RelativeMemberInfo>,
): Map<string, AccountUserInfo> {
const memberMap = new Map<string, AccountUserInfo>();
for (const member of members) {
const { id, role, username } = member;
if (!role || !username) {
continue;
}
memberMap.set(username.toLowerCase(), { id, username });
}
return memberMap;
}
// Returns map from user ID to AccountUserInfo
function extractMentionedMembers(
text: string,
threadInfo: ThreadInfo,
): Map<string, AccountUserInfo> {
const memberMap = memberLowercaseUsernameMap(threadInfo.members);
const mentions = extractUserMentionsFromText(text);
const mentionedMembers = new Map<string, AccountUserInfo>();
for (const mention of mentions) {
const userInfo = memberMap.get(mention.toLowerCase());
if (userInfo) {
mentionedMembers.set(userInfo.id, userInfo);
}
}
return mentionedMembers;
}
// When a member of the parent is mentioned in a sidebar,
// they will be automatically added to that sidebar
function extractNewMentionedParentMembers(
messageText: string,
threadInfo: ThreadInfo,
parentThreadInfo: ThreadInfo,
): AccountUserInfo[] {
const mentionedMembersOfParent = extractMentionedMembers(
messageText,
parentThreadInfo,
);
for (const member of threadInfo.members) {
if (member.role) {
mentionedMembersOfParent.delete(member.id);
}
}
return [...mentionedMembersOfParent.values()];
}
function pendingThreadType(
numberOfOtherMembers: number,
thickOrThin: 'thick' | 'thin',
): 4 | 6 | 7 | 13 | 14 | 15 {
if (thickOrThin === 'thick') {
if (numberOfOtherMembers === 0) {
return threadTypes.PRIVATE;
} else if (numberOfOtherMembers === 1) {
return threadTypes.PERSONAL;
} else {
return threadTypes.LOCAL;
}
} else {
if (numberOfOtherMembers === 0) {
return threadTypes.GENESIS_PRIVATE;
} else if (numberOfOtherMembers === 1) {
return threadTypes.GENESIS_PERSONAL;
} else {
return threadTypes.COMMUNITY_SECRET_SUBTHREAD;
}
}
}
function threadTypeCanBePending(threadType: ThreadType): boolean {
return (
threadType === threadTypes.GENESIS_PERSONAL ||
threadType === threadTypes.COMMUNITY_SECRET_SUBTHREAD ||
threadType === threadTypes.SIDEBAR ||
threadType === threadTypes.GENESIS_PRIVATE ||
threadType === threadTypes.PERSONAL ||
threadType === threadTypes.LOCAL ||
threadType === threadTypes.THICK_SIDEBAR ||
threadType === threadTypes.PRIVATE
);
}
type RawThreadInfoOptions = {
+filterThreadEditAvatarPermission?: boolean,
+excludePinInfo?: boolean,
+filterManageInviteLinksPermission?: boolean,
+filterVoicedInAnnouncementChannelsPermission?: boolean,
+minimallyEncodePermissions?: boolean,
+includeSpecialRoleFieldInRoles?: boolean,
+allowAddingUsersToCommunityRoot?: boolean,
+filterManageFarcasterChannelTagsPermission?: boolean,
+stripMemberPermissions?: boolean,
+canDisplayFarcasterThreadAvatars?: boolean,
+ +dontFilterMissingKnowOf?: boolean,
};
function rawThreadInfoFromServerThreadInfo(
serverThreadInfo: ServerThreadInfo,
viewerID: string,
options?: RawThreadInfoOptions,
): ?LegacyThinRawThreadInfo | ?ThinRawThreadInfo {
const filterThreadEditAvatarPermission =
options?.filterThreadEditAvatarPermission;
const excludePinInfo = options?.excludePinInfo;
const filterManageInviteLinksPermission =
options?.filterManageInviteLinksPermission;
const filterVoicedInAnnouncementChannelsPermission =
options?.filterVoicedInAnnouncementChannelsPermission;
const shouldMinimallyEncodePermissions = options?.minimallyEncodePermissions;
const shouldIncludeSpecialRoleFieldInRoles =
options?.includeSpecialRoleFieldInRoles;
const allowAddingUsersToCommunityRoot =
options?.allowAddingUsersToCommunityRoot;
const filterManageFarcasterChannelTagsPermission =
options?.filterManageFarcasterChannelTagsPermission;
const stripMemberPermissions = options?.stripMemberPermissions;
const canDisplayFarcasterThreadAvatars =
options?.canDisplayFarcasterThreadAvatars;
+ const dontFilterMissingKnowOf = options?.dontFilterMissingKnowOf ?? false;
const filterThreadPermissions = (
innerThreadPermissions: ThreadPermissionsInfo,
) => {
if (
allowAddingUsersToCommunityRoot &&
(serverThreadInfo.type === threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT ||
serverThreadInfo.type === threadTypes.COMMUNITY_ROOT)
) {
innerThreadPermissions = {
...innerThreadPermissions,
[threadPermissions.ADD_MEMBERS]: {
value: true,
source: serverThreadInfo.id,
},
};
}
return _omitBy(
(v, k) =>
(filterThreadEditAvatarPermission &&
[
threadPermissions.EDIT_THREAD_AVATAR,
threadPermissionPropagationPrefixes.DESCENDANT +
threadPermissions.EDIT_THREAD_AVATAR,
].includes(k)) ||
(excludePinInfo &&
[
threadPermissions.MANAGE_PINS,
threadPermissionPropagationPrefixes.DESCENDANT +
threadPermissions.MANAGE_PINS,
].includes(k)) ||
(filterManageInviteLinksPermission &&
[threadPermissions.MANAGE_INVITE_LINKS].includes(k)) ||
(filterVoicedInAnnouncementChannelsPermission &&
[
threadPermissions.VOICED_IN_ANNOUNCEMENT_CHANNELS,
threadPermissionPropagationPrefixes.DESCENDANT +
threadPermissionFilterPrefixes.TOP_LEVEL +
threadPermissions.VOICED_IN_ANNOUNCEMENT_CHANNELS,
].includes(k)) ||
(filterManageFarcasterChannelTagsPermission &&
[threadPermissions.MANAGE_FARCASTER_CHANNEL_TAGS].includes(k)),
)(innerThreadPermissions);
};
const members = [];
let currentUser;
for (const serverMember of serverThreadInfo.members) {
if (
serverThreadInfo.id === genesis().id &&
serverMember.id !== viewerID &&
serverMember.id !== ashoat.id
) {
continue;
}
const memberPermissions = filterThreadPermissions(serverMember.permissions);
members.push({
id: serverMember.id,
role: serverMember.role,
permissions: memberPermissions,
isSender: serverMember.isSender,
});
if (serverMember.id === viewerID) {
currentUser = {
role: serverMember.role,
permissions: memberPermissions,
subscription: serverMember.subscription,
unread: serverMember.unread,
};
}
}
let currentUserPermissions;
if (currentUser) {
currentUserPermissions = currentUser.permissions;
} else {
currentUserPermissions = filterThreadPermissions(
getAllThreadPermissions(null, serverThreadInfo.id),
);
currentUser = {
role: null,
permissions: currentUserPermissions,
subscription: defaultThreadSubscription,
unread: null,
};
}
- if (!permissionLookup(currentUserPermissions, threadPermissions.KNOW_OF)) {
+ if (
+ !dontFilterMissingKnowOf &&
+ !permissionLookup(currentUserPermissions, threadPermissions.KNOW_OF)
+ ) {
return null;
}
const rolesWithFilteredThreadPermissions = _mapValues(role => ({
...role,
permissions: filterThreadPermissions(role.permissions),
}))(serverThreadInfo.roles);
const rolesWithoutSpecialRoleField = _mapValues(role => {
const { specialRole, ...roleSansSpecialRole } = role;
return roleSansSpecialRole;
})(rolesWithFilteredThreadPermissions);
let rawThreadInfo: any = {
id: serverThreadInfo.id,
type: serverThreadInfo.type,
name: serverThreadInfo.name,
description: serverThreadInfo.description,
color: serverThreadInfo.color,
creationTime: serverThreadInfo.creationTime,
parentThreadID: serverThreadInfo.parentThreadID,
members,
roles: rolesWithoutSpecialRoleField,
currentUser,
repliesCount: serverThreadInfo.repliesCount,
containingThreadID: serverThreadInfo.containingThreadID,
community: serverThreadInfo.community,
};
const sourceMessageID = serverThreadInfo.sourceMessageID;
if (sourceMessageID) {
rawThreadInfo = { ...rawThreadInfo, sourceMessageID };
}
if (serverThreadInfo.avatar) {
const avatar =
serverThreadInfo.avatar.type === 'farcaster' &&
!canDisplayFarcasterThreadAvatars
? null
: serverThreadInfo.avatar;
rawThreadInfo = { ...rawThreadInfo, avatar };
}
if (!excludePinInfo) {
rawThreadInfo = {
...rawThreadInfo,
pinnedCount: serverThreadInfo.pinnedCount,
};
}
if (!shouldMinimallyEncodePermissions) {
return rawThreadInfo;
}
const minimallyEncodedRawThreadInfoWithMemberPermissions =
minimallyEncodeRawThreadInfoWithMemberPermissions(rawThreadInfo);
invariant(
!minimallyEncodedRawThreadInfoWithMemberPermissions.thick,
'ServerThreadInfo should be thin thread',
);
if (!shouldIncludeSpecialRoleFieldInRoles) {
const minimallyEncodedRolesWithoutSpecialRoleField = Object.fromEntries(
entries(minimallyEncodedRawThreadInfoWithMemberPermissions.roles).map(
([key, role]) => [
key,
{
..._omit('specialRole')(role),
isDefault: roleIsDefaultRole(role),
},
],
),
);
return {
...minimallyEncodedRawThreadInfoWithMemberPermissions,
roles: minimallyEncodedRolesWithoutSpecialRoleField,
};
}
if (!stripMemberPermissions) {
return minimallyEncodedRawThreadInfoWithMemberPermissions;
}
// The return value of `deprecatedMinimallyEncodeRawThreadInfo` is typed
// as `RawThreadInfo`, but still includes thread member permissions.
// This was to prevent introducing "Legacy" types that would need to be
// maintained going forward. This `any`-cast allows us to more precisely
// type the obj being passed to `stripMemberPermissionsFromRawThreadInfo`.
const rawThreadInfoWithMemberPermissions: ThinRawThreadInfoWithPermissions =
(minimallyEncodedRawThreadInfoWithMemberPermissions: any);
return stripMemberPermissionsFromRawThreadInfo(
rawThreadInfoWithMemberPermissions,
);
}
function threadUIName(threadInfo: ThreadInfo): string | ThreadEntity {
if (threadInfo.name) {
return firstLine(threadInfo.name);
}
const threadMembers: $ReadOnlyArray<RelativeMemberInfo> =
threadInfo.members.filter(memberInfo => memberInfo.role);
const memberEntities: $ReadOnlyArray<UserEntity> = threadMembers.map(member =>
ET.user({ userInfo: member }),
);
return {
type: 'thread',
id: threadInfo.id,
name: threadInfo.name,
display: 'uiName',
uiName: memberEntities,
ifJustViewer: threadTypeIsPrivate(threadInfo.type)
? 'viewer_username'
: 'just_you_string',
};
}
function threadInfoFromRawThreadInfo(
rawThreadInfo: RawThreadInfo,
viewerID: ?string,
userInfos: UserInfos,
): ThreadInfo {
let threadInfo: ThreadInfo = {
minimallyEncoded: true,
id: rawThreadInfo.id,
type: rawThreadInfo.type,
name: rawThreadInfo.name,
uiName: '',
description: rawThreadInfo.description,
color: rawThreadInfo.color,
creationTime: rawThreadInfo.creationTime,
parentThreadID: rawThreadInfo.parentThreadID,
containingThreadID: rawThreadInfo.containingThreadID,
community: rawThreadInfo.community,
members: getRelativeMemberInfos(rawThreadInfo, viewerID, userInfos),
roles: rawThreadInfo.roles,
currentUser: rawThreadInfo.currentUser,
repliesCount: rawThreadInfo.repliesCount,
};
threadInfo = {
...threadInfo,
uiName: threadUIName(threadInfo),
};
const { sourceMessageID, avatar, pinnedCount } = rawThreadInfo;
if (sourceMessageID) {
threadInfo = { ...threadInfo, sourceMessageID };
}
if (avatar) {
threadInfo = { ...threadInfo, avatar };
} else if (
threadTypeIsPrivate(rawThreadInfo.type) ||
threadTypeIsPersonal(rawThreadInfo.type)
) {
threadInfo = {
...threadInfo,
avatar: getUserAvatarForThread(rawThreadInfo, viewerID, userInfos),
};
}
if (pinnedCount) {
threadInfo = { ...threadInfo, pinnedCount };
}
return threadInfo;
}
function filterOutDisabledPermissions(permissionsBitmask: string): string {
const decodedPermissions: ThreadPermissionsInfo =
threadPermissionsFromBitmaskHex(permissionsBitmask);
const updatedPermissions = { ...decodedPermissions, ...disabledPermissions };
const encodedUpdatedPermissions: string =
permissionsToBitmaskHex(updatedPermissions);
return encodedUpdatedPermissions;
}
function baseThreadIsWithBlockedUserOnly(
threadInfo: LegacyRawThreadInfo | RawThreadInfo | ThreadInfo,
viewerID: ?string,
userInfos: UserInfos,
checkOnlyViewerBlock: boolean,
) {
const otherUserID = getSingleOtherUser(threadInfo, viewerID);
if (!otherUserID) {
return false;
}
const otherUserRelationshipStatus =
userInfos[otherUserID]?.relationshipStatus;
if (checkOnlyViewerBlock) {
return (
otherUserRelationshipStatus === userRelationshipStatus.BLOCKED_BY_VIEWER
);
}
return (
!!otherUserRelationshipStatus &&
relationshipBlockedInEitherDirection(otherUserRelationshipStatus)
);
}
function threadIsWithBlockedUserOnlyWithoutAdminRoleCheck(
threadInfo: ThreadInfo | RawThreadInfo | LegacyRawThreadInfo,
viewerID: ?string,
userInfos: UserInfos,
checkOnlyViewerBlock: boolean,
): boolean {
if (threadOrParentThreadIsGroupChat(threadInfo)) {
return false;
}
return baseThreadIsWithBlockedUserOnly(
threadInfo,
viewerID,
userInfos,
checkOnlyViewerBlock,
);
}
function useThreadFrozenDueToViewerBlock(
threadInfo: ThreadInfo,
communityThreadInfo: ?ThreadInfo,
viewerID: ?string,
userInfos: UserInfos,
): boolean {
const communityThreadInfoArray = React.useMemo(
() => (communityThreadInfo ? [communityThreadInfo] : []),
[communityThreadInfo],
);
const communityRootsMembersToRole = useCommunityRootMembersToRole(
communityThreadInfoArray,
);
const memberToRole = communityRootsMembersToRole[communityThreadInfo?.id];
const memberHasAdminRole = threadMembersWithoutAddedAdmin(threadInfo).some(
m => roleIsAdminRole(memberToRole?.[m.id]),
);
return React.useMemo(() => {
if (memberHasAdminRole) {
return false;
}
return threadIsWithBlockedUserOnlyWithoutAdminRoleCheck(
threadInfo,
viewerID,
userInfos,
true,
);
}, [memberHasAdminRole, threadInfo, userInfos, viewerID]);
}
const threadTypeDescriptions: { [ThreadType]: string } = {
[threadTypes.COMMUNITY_OPEN_SUBTHREAD]:
'Anybody in the parent channel can see an open subchannel.',
[threadTypes.COMMUNITY_SECRET_SUBTHREAD]:
'Only visible to its members and admins of ancestor channels.',
};
function roleIsDefaultRole(
roleInfo: ?ClientLegacyRoleInfo | ?RoleInfo,
): boolean {
if (roleInfo?.specialRole === specialRoles.DEFAULT_ROLE) {
return true;
}
return !!(roleInfo && roleInfo.isDefault);
}
function roleIsAdminRole(roleInfo: ?ClientLegacyRoleInfo | ?RoleInfo): boolean {
if (roleInfo?.specialRole === specialRoles.ADMIN_ROLE) {
return true;
}
return !!(roleInfo && !roleInfo.isDefault && roleInfo.name === 'Admins');
}
function threadHasAdminRole(
threadInfo: ?(
| LegacyRawThreadInfo
| RawThreadInfo
| ThreadInfo
| ServerThreadInfo
),
): boolean {
if (!threadInfo) {
return false;
}
let hasSpecialRoleFieldBeenEncountered = false;
for (const role of Object.values(threadInfo.roles)) {
if (role.specialRole === specialRoles.ADMIN_ROLE) {
return true;
}
if (role.specialRole !== undefined) {
hasSpecialRoleFieldBeenEncountered = true;
}
}
if (hasSpecialRoleFieldBeenEncountered) {
return false;
}
return !!_find({ name: 'Admins' })(threadInfo.roles);
}
function identifyInvalidatedThreads(
updateInfos: $ReadOnlyArray<ClientUpdateInfo>,
): Set<string> {
const invalidated = new Set<string>();
for (const updateInfo of updateInfos) {
if (updateInfo.type === updateTypes.DELETE_THREAD) {
invalidated.add(updateInfo.threadID);
}
}
return invalidated;
}
const permissionsDisabledByBlockArray = values(
threadPermissionsDisabledByBlock,
);
const permissionsDisabledByBlock: Set<ThreadPermission> = new Set(
permissionsDisabledByBlockArray,
);
const disabledPermissions: ThreadPermissionsInfo =
permissionsDisabledByBlockArray.reduce(
(permissions: ThreadPermissionsInfo, permission: string) => ({
...permissions,
[permission]: { value: false, source: null },
}),
{},
);
// Consider updating itemHeight in native/chat/chat-thread-list.react.js
// if you change this
const emptyItemText: string =
`Muted chats are just like normal chats, except they don't ` +
`contribute to your unread count.\n\n` +
`To move a chat over here, switch the “Muted” option in its settings.`;
function threadNoun(threadType: ThreadType, parentThreadID: ?string): string {
if (threadTypeIsSidebar(threadType)) {
return 'thread';
} else if (
threadType === threadTypes.COMMUNITY_SECRET_SUBTHREAD &&
parentThreadID === genesis().id
) {
return 'chat';
} else if (
threadType === threadTypes.COMMUNITY_ROOT ||
threadType === threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT ||
threadType === threadTypes.COMMUNITY_OPEN_SUBTHREAD ||
threadType === threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD ||
threadType === threadTypes.COMMUNITY_SECRET_SUBTHREAD ||
threadType === threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD ||
threadType === threadTypes.GENESIS
) {
return 'channel';
} else {
return 'chat';
}
}
function threadLabel(threadType: ThreadType): string {
if (
threadType === threadTypes.COMMUNITY_OPEN_SUBTHREAD ||
threadType === threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD
) {
return 'Open';
} else if (threadType === threadTypes.GENESIS_PERSONAL) {
return 'Personal';
} else if (threadTypeIsSidebar(threadType)) {
return 'Thread';
} else if (threadType === threadTypes.GENESIS_PRIVATE) {
return 'Private';
} else if (
threadType === threadTypes.COMMUNITY_ROOT ||
threadType === threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT ||
threadType === threadTypes.GENESIS
) {
return 'Community';
} else if (threadTypeIsThick(threadType)) {
return 'Local DM';
} else {
return 'Secret';
}
}
type ExistingThreadInfoFinderParams = {
+searching: boolean,
+userInfoInputArray: $ReadOnlyArray<AccountUserInfo>,
+allUsersSupportThickThreads: boolean,
};
type ExistingThreadInfoFinder = (
params: ExistingThreadInfoFinderParams,
) => ?ThreadInfo;
function useExistingThreadInfoFinder(
baseThreadInfo: ?ThreadInfo,
): ExistingThreadInfoFinder {
const threadInfos = useSelector(threadInfoSelector);
const loggedInUserInfo = useLoggedInUserInfo();
const pendingToRealizedThreadIDs = useSelector(state =>
pendingToRealizedThreadIDsSelector(state.threadStore.threadInfos),
);
return React.useCallback(
(params: ExistingThreadInfoFinderParams): ?ThreadInfo => {
if (!baseThreadInfo) {
return null;
}
const realizedThreadInfo = threadInfos[baseThreadInfo.id];
if (realizedThreadInfo) {
return realizedThreadInfo;
}
if (!loggedInUserInfo || !threadIsPending(baseThreadInfo.id)) {
return baseThreadInfo;
}
const viewerID = loggedInUserInfo?.id;
invariant(
threadTypeCanBePending(baseThreadInfo.type),
`ThreadInfo has pending ID ${baseThreadInfo.id}, but type that ` +
`should not be pending ${baseThreadInfo.type}`,
);
const { searching, userInfoInputArray } = params;
const { sourceMessageID } = baseThreadInfo;
let pendingThreadID;
if (searching) {
pendingThreadID = getPendingThreadID(
pendingThreadType(userInfoInputArray.length, 'thick'),
[...userInfoInputArray.map(user => user.id), viewerID],
sourceMessageID,
);
} else {
pendingThreadID = getPendingThreadID(
baseThreadInfo.type,
baseThreadInfo.members.map(member => member.id),
sourceMessageID,
);
}
const realizedThreadID = pendingToRealizedThreadIDs.get(pendingThreadID);
if (realizedThreadID && threadInfos[realizedThreadID]) {
return threadInfos[realizedThreadID];
}
if (!searching) {
return baseThreadInfo;
}
return createPendingThread({
viewerID,
threadType: pendingThreadType(
userInfoInputArray.length,
params.allUsersSupportThickThreads ? 'thick' : 'thin',
),
members: [
{ ...loggedInUserInfo, isViewer: true },
...userInfoInputArray,
],
});
},
[baseThreadInfo, threadInfos, loggedInUserInfo, pendingToRealizedThreadIDs],
);
}
type ThreadTypeParentRequirement = 'optional' | 'required' | 'disabled';
function getThreadTypeParentRequirement(
threadType: ThinThreadType,
): ThreadTypeParentRequirement {
if (
threadType === threadTypes.COMMUNITY_OPEN_SUBTHREAD ||
threadType === threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD ||
//threadType === threadTypes.COMMUNITY_SECRET_SUBTHREAD ||
threadType === threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD ||
threadTypeIsSidebar(threadType)
) {
return 'required';
} else if (
threadType === threadTypes.COMMUNITY_ROOT ||
threadType === threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT ||
threadType === threadTypes.GENESIS ||
threadType === threadTypes.GENESIS_PERSONAL ||
threadType === threadTypes.GENESIS_PRIVATE
) {
return 'disabled';
} else {
return 'optional';
}
}
function checkIfDefaultMembersAreVoiced(threadInfo: ThreadInfo): boolean {
const defaultRoleID = Object.keys(threadInfo.roles).find(roleID =>
roleIsDefaultRole(threadInfo.roles[roleID]),
);
invariant(
defaultRoleID !== undefined,
'all threads should have a default role',
);
const defaultRole = threadInfo.roles[defaultRoleID];
const defaultRolePermissions =
decodeMinimallyEncodedRoleInfo(defaultRole).permissions;
return !!defaultRolePermissions[threadPermissions.VOICED];
}
const draftKeySuffix = '/message_composer';
function draftKeyFromThreadID(threadID: string): string {
return `${threadID}${draftKeySuffix}`;
}
function getContainingThreadID(
parentThreadInfo:
| ?ServerThreadInfo
| LegacyRawThreadInfo
| RawThreadInfo
| ThreadInfo,
threadType: ThreadType,
): ?string {
if (!parentThreadInfo) {
return null;
}
if (threadTypeIsSidebar(threadType)) {
return parentThreadInfo.id;
}
if (!parentThreadInfo.containingThreadID) {
return parentThreadInfo.id;
}
return parentThreadInfo.containingThreadID;
}
function getCommunity(
threadInfo:
| ?ServerThreadInfo
| LegacyRawThreadInfo
| RawThreadInfo
| ThreadInfo,
): ?string {
if (!threadInfo) {
return null;
}
const { id, community, type } = threadInfo;
if (community !== null && community !== undefined) {
return community;
}
if (threadTypeIsCommunityRoot(type)) {
return id;
}
return null;
}
function getSearchResultsForEmptySearchText(
chatListData: $ReadOnlyArray<ChatThreadItem>,
threadFilter: ThreadInfo => boolean,
): Array<ChatThreadItem> {
const threadHomeSubscriptions = Object.fromEntries(
chatListData.map(chatThreadItem => [
chatThreadItem.threadInfo.id,
threadIsInHome(chatThreadItem.threadInfo),
]),
);
return chatListData
.filter((item: ChatThreadItem) => {
const { threadInfo } = item;
const { parentThreadID } = threadInfo;
const isInFilteredChatList =
threadInChatList(threadInfo) && threadFilter(threadInfo);
if (!isInFilteredChatList) {
return false;
}
if (!threadIsSidebar(threadInfo) || !parentThreadID) {
return true;
}
const isParentInHome = threadHomeSubscriptions[parentThreadID];
const isThreadInHome = threadIsInHome(threadInfo);
return isParentInHome !== isThreadInHome;
})
.map((item: ChatThreadItem) => {
if (threadIsSidebar(item.threadInfo)) {
return item;
}
const sidebarsOnlyInSameTab = item.sidebars.filter(
(sidebar: SidebarItem) =>
sidebar.type !== 'sidebar' ||
threadIsInHome(sidebar.threadInfo) ===
threadIsInHome(item.threadInfo),
);
if (sidebarsOnlyInSameTab.length === item.sidebars.length) {
return item;
}
return {
...item,
sidebars: sidebarsOnlyInSameTab,
};
});
}
function getThreadListSearchResults(
chatListData: $ReadOnlyArray<ChatThreadItem>,
searchText: string,
threadFilter: ThreadInfo => boolean,
threadSearchResults: $ReadOnlySet<string>,
usersSearchResults: $ReadOnlyArray<UserSearchResult>,
loggedInUserInfo: ?LoggedInUserInfo,
): $ReadOnlyArray<ChatThreadItem> {
if (!searchText) {
return getSearchResultsForEmptySearchText(chatListData, threadFilter);
}
const privateThreads = [];
const personalThreads = [];
const otherThreads = [];
for (const item of chatListData) {
if (!threadSearchResults.has(item.threadInfo.id)) {
continue;
}
if (threadTypeIsPrivate(item.threadInfo.type)) {
privateThreads.push({ ...item, sidebars: [] });
} else if (threadTypeIsPersonal(item.threadInfo.type)) {
personalThreads.push({ ...item, sidebars: [] });
} else {
otherThreads.push({ ...item, sidebars: [] });
}
}
const chatItems: ChatThreadItem[] = [
...privateThreads,
...personalThreads,
...otherThreads,
];
if (loggedInUserInfo) {
chatItems.push(
...usersSearchResults.map(user =>
createPendingThreadItem(
loggedInUserInfo,
user,
user.supportThickThreads,
),
),
);
}
return chatItems;
}
function reorderThreadSearchResults<T: RawThreadInfo | ThreadInfo>(
threadInfos: $ReadOnlyArray<T>,
threadSearchResults: $ReadOnlyArray<string>,
): T[] {
const privateThreads = [];
const personalThreads = [];
const otherThreads = [];
const threadSearchResultsSet = new Set(threadSearchResults);
for (const threadInfo of threadInfos) {
if (!threadSearchResultsSet.has(threadInfo.id)) {
continue;
}
if (threadTypeIsPrivate(threadInfo.type)) {
privateThreads.push(threadInfo);
} else if (threadTypeIsPersonal(threadInfo.type)) {
personalThreads.push(threadInfo);
} else {
otherThreads.push(threadInfo);
}
}
return [...privateThreads, ...personalThreads, ...otherThreads];
}
function useAvailableThreadMemberActions(
memberInfo: RelativeMemberInfo,
threadInfo: ThreadInfo,
canEdit: ?boolean = true,
): $ReadOnlyArray<'change_role' | 'remove_user'> {
const canRemoveMembers = useThreadHasPermission(
threadInfo,
threadPermissions.REMOVE_MEMBERS,
);
const canChangeRoles = useThreadHasPermission(
threadInfo,
threadPermissions.CHANGE_ROLE,
);
return React.useMemo(() => {
const { role } = memberInfo;
if (!canEdit || !role) {
return [];
}
const result = [];
if (
canChangeRoles &&
memberInfo.username &&
threadHasAdminRole(threadInfo)
) {
result.push('change_role');
}
if (
canRemoveMembers &&
!memberInfo.isViewer &&
(canChangeRoles || roleIsDefaultRole(threadInfo.roles[role]))
) {
result.push('remove_user');
}
return result;
}, [canChangeRoles, canEdit, canRemoveMembers, memberInfo, threadInfo]);
}
function patchThreadInfoToIncludeMentionedMembersOfParent(
threadInfo: ThreadInfo,
parentThreadInfo: ThreadInfo,
messageText: string,
viewerID: string,
): ThreadInfo {
const members: UserIDAndUsername[] = threadInfo.members
.map(({ id, username }) =>
username ? ({ id, username }: UserIDAndUsername) : null,
)
.filter(Boolean);
const mentionedNewMembers = extractNewMentionedParentMembers(
messageText,
threadInfo,
parentThreadInfo,
);
if (mentionedNewMembers.length === 0) {
return threadInfo;
}
members.push(...mentionedNewMembers);
const threadType = threadTypeIsThick(parentThreadInfo.type)
? threadTypes.THICK_SIDEBAR
: threadTypes.SIDEBAR;
return createPendingThread({
viewerID,
threadType,
members,
parentThreadInfo,
threadColor: threadInfo.color,
name: threadInfo.name,
sourceMessageID: threadInfo.sourceMessageID,
});
}
type RoleAndMemberCount = {
[roleName: string]: number,
};
function useRoleMemberCountsForCommunity(
threadInfo: ThreadInfo,
): RoleAndMemberCount {
return React.useMemo(() => {
const roleIDsToNames: { [string]: string } = {};
Object.keys(threadInfo.roles).forEach(roleID => {
roleIDsToNames[roleID] = threadInfo.roles[roleID].name;
});
const roleNamesToMemberCount: RoleAndMemberCount = {};
threadInfo.members.forEach(({ role: roleID }) => {
invariant(roleID, 'Community member should have a role');
const roleName = roleIDsToNames[roleID];
roleNamesToMemberCount[roleName] =
(roleNamesToMemberCount[roleName] ?? 0) + 1;
});
// For all community roles with no members, add them to the list with 0
Object.keys(roleIDsToNames).forEach(roleName => {
if (roleNamesToMemberCount[roleIDsToNames[roleName]] === undefined) {
roleNamesToMemberCount[roleIDsToNames[roleName]] = 0;
}
});
return roleNamesToMemberCount;
}, [threadInfo.members, threadInfo.roles]);
}
function useRoleNamesToSpecialRole(threadInfo: ThreadInfo): {
+[roleName: string]: ?SpecialRole,
} {
return React.useMemo(() => {
const roleNamesToSpecialRole: { [roleName: string]: ?SpecialRole } = {};
values(threadInfo.roles).forEach(role => {
if (roleNamesToSpecialRole[role.name] !== undefined) {
return;
}
if (roleIsDefaultRole(role)) {
roleNamesToSpecialRole[role.name] = specialRoles.DEFAULT_ROLE;
} else if (roleIsAdminRole(role)) {
roleNamesToSpecialRole[role.name] = specialRoles.ADMIN_ROLE;
} else {
roleNamesToSpecialRole[role.name] = null;
}
});
return roleNamesToSpecialRole;
}, [threadInfo.roles]);
}
type RoleUserSurfacedPermissions = {
+[roleName: string]: $ReadOnlySet<UserSurfacedPermission>,
};
// Iterates through the existing roles in the community and 'reverse maps'
// the set of permission literals for each role to user-facing permission enums
// to help pre-populate the permission checkboxes when editing roles.
function useRoleUserSurfacedPermissions(
threadInfo: ThreadInfo,
): RoleUserSurfacedPermissions {
return React.useMemo(() => {
const roleNamesToPermissions: { [string]: Set<UserSurfacedPermission> } =
{};
Object.keys(threadInfo.roles).forEach(roleID => {
const roleName = threadInfo.roles[roleID].name;
const rolePermissions = decodeMinimallyEncodedRoleInfo(
threadInfo.roles[roleID],
).permissions;
roleNamesToPermissions[roleName] =
userSurfacedPermissionsFromRolePermissions(rolePermissions);
});
return roleNamesToPermissions;
}, [threadInfo.roles]);
}
function communityOrThreadNoun(threadInfo: RawThreadInfo | ThreadInfo): string {
return threadTypeIsCommunityRoot(threadInfo.type)
? 'community'
: threadNoun(threadInfo.type, threadInfo.parentThreadID);
}
function getThreadsToDeleteText(
threadInfo: RawThreadInfo | ThreadInfo,
): string {
return `${
threadTypeIsCommunityRoot(threadInfo.type)
? 'Subchannels and threads'
: 'Threads'
} within this ${communityOrThreadNoun(threadInfo)}`;
}
type OldestCreatedInput = { +creationTime: number, ... };
function getOldestCreated<T: OldestCreatedInput>(arr: $ReadOnlyArray<T>): ?T {
return arr.reduce<?T>(
(a, b) => (!b || (a && a.creationTime < b.creationTime) ? a : b),
null,
);
}
function useOldestPrivateThreadInfo(): ?ThreadInfo {
const genesisPrivateThreadInfosSelector = threadInfosSelectorForThreadType(
threadTypes.GENESIS_PRIVATE,
);
const genesisPrivateThreadInfos = useSelector(
genesisPrivateThreadInfosSelector,
);
const privateThreadInfosSelector = threadInfosSelectorForThreadType(
threadTypes.PRIVATE,
);
const privateThreadInfos = useSelector(privateThreadInfosSelector);
return React.useMemo(
() =>
getOldestCreated([...privateThreadInfos, ...genesisPrivateThreadInfos]),
[privateThreadInfos, genesisPrivateThreadInfos],
);
}
function useUserProfileThreadInfo(userInfo: ?UserInfo): ?UserProfileThreadInfo {
const userID = userInfo?.id;
const username = userInfo?.username;
const loggedInUserInfo = useLoggedInUserInfo();
const isViewerProfile = loggedInUserInfo?.id === userID;
const oldestPrivateThreadInfo = useOldestPrivateThreadInfo();
const usersWithPersonalThread = useSelector(usersWithPersonalThreadSelector);
const genesisPersonalThreadInfosSelector = threadInfosSelectorForThreadType(
threadTypes.GENESIS_PERSONAL,
);
const genesisPersonalThreadInfos = useSelector(
genesisPersonalThreadInfosSelector,
);
const personalThreadInfosSelector = threadInfosSelectorForThreadType(
threadTypes.PERSONAL,
);
const personalThreadInfos = useSelector(personalThreadInfosSelector);
const allPersonalThreadInfos = React.useMemo(
() => [...personalThreadInfos, ...genesisPersonalThreadInfos],
[personalThreadInfos, genesisPersonalThreadInfos],
);
const [supportThickThreads, setSupportThickThreads] = React.useState(false);
const usersSupportThickThreads = useUsersSupportThickThreads();
React.useEffect(() => {
void (async () => {
if (!userInfo) {
setSupportThickThreads(false);
return;
}
const result = await usersSupportThickThreads([userInfo.id]);
setSupportThickThreads(result.has(userInfo.id));
})();
}, [userInfo, usersSupportThickThreads]);
return React.useMemo(() => {
if (!loggedInUserInfo || !userID || !username) {
return null;
}
if (isViewerProfile && oldestPrivateThreadInfo) {
return { threadInfo: oldestPrivateThreadInfo };
}
if (usersWithPersonalThread.has(userID)) {
const personalThreadInfo: ?ThreadInfo = getOldestCreated(
allPersonalThreadInfos.filter(
threadInfo =>
userID === getSingleOtherUser(threadInfo, loggedInUserInfo.id),
),
);
return personalThreadInfo ? { threadInfo: personalThreadInfo } : null;
}
const pendingPersonalThreadInfo = createPendingPersonalOrPrivateThread(
loggedInUserInfo,
userID,
username,
supportThickThreads,
);
return pendingPersonalThreadInfo;
}, [
isViewerProfile,
loggedInUserInfo,
allPersonalThreadInfos,
oldestPrivateThreadInfo,
supportThickThreads,
userID,
username,
usersWithPersonalThread,
]);
}
function assertAllThreadInfosAreLegacy(rawThreadInfos: MixedRawThreadInfos): {
[id: string]: LegacyRawThreadInfo,
} {
return Object.fromEntries(
Object.entries(rawThreadInfos).map(([id, rawThreadInfo]) => {
invariant(
!rawThreadInfo.minimallyEncoded,
`rawThreadInfos shouldn't be minimallyEncoded`,
);
return [id, rawThreadInfo];
}),
);
}
function useOnScreenEntryEditableThreadInfos(): $ReadOnlyArray<ThreadInfo> {
const visibleThreadInfos = useSelector(onScreenThreadInfos);
const editableVisibleThreadInfos = useThreadsWithPermission(
visibleThreadInfos,
threadPermissions.EDIT_ENTRIES,
);
return editableVisibleThreadInfos;
}
function createThreadTimestamps(
timestamp: number,
memberIDs: $ReadOnlyArray<string>,
): ThreadTimestamps {
return {
name: timestamp,
avatar: timestamp,
description: timestamp,
color: timestamp,
members: Object.fromEntries(
memberIDs.map(id => [
id,
{ isMember: timestamp, subscription: timestamp },
]),
),
currentUser: {
unread: timestamp,
},
};
}
function userHasDeviceList(
userID: string,
auxUserInfos: AuxUserInfos,
): boolean {
return deviceListIsNonEmpty(auxUserInfos[userID]?.deviceList);
}
function deviceListIsNonEmpty(deviceList?: RawDeviceList): boolean {
return !!deviceList && deviceList.devices.length > 0;
}
const deviceListRequestTimeout = 20 * 1000; // twenty seconds
const expectedAccountDeletionUpdateTimeout = 24 * 60 * 60 * 1000; // one day
function deviceListCanBeRequestedForUser(
userID: string,
auxUserInfos: AuxUserInfos,
): boolean {
return (
!auxUserInfos[userID]?.accountMissingStatus ||
auxUserInfos[userID].accountMissingStatus.lastChecked <
Date.now() - deviceListRequestTimeout
);
}
export {
threadHasPermission,
useCommunityRootMembersToRole,
useThreadHasPermission,
viewerIsMember,
threadInChatList,
useIsThreadInChatList,
useThreadsInChatList,
threadIsTopLevel,
threadIsChannel,
threadIsSidebar,
threadInBackgroundChatList,
threadInHomeChatList,
threadIsInHome,
threadInFilterList,
userIsMember,
threadActualMembers,
threadOtherMembers,
threadIsGroupChat,
threadIsPending,
threadIsPendingSidebar,
getSingleOtherUser,
getPendingThreadID,
parsePendingThreadID,
createPendingThread,
extractNewMentionedParentMembers,
pendingThreadType,
filterOutDisabledPermissions,
useThreadFrozenDueToViewerBlock,
rawThreadInfoFromServerThreadInfo,
threadUIName,
threadInfoFromRawThreadInfo,
threadTypeDescriptions,
threadIsWithBlockedUserOnlyWithoutAdminRoleCheck,
roleIsDefaultRole,
roleIsAdminRole,
threadHasAdminRole,
identifyInvalidatedThreads,
permissionsDisabledByBlock,
emptyItemText,
threadNoun,
threadLabel,
useExistingThreadInfoFinder,
getThreadTypeParentRequirement,
checkIfDefaultMembersAreVoiced,
draftKeySuffix,
draftKeyFromThreadID,
threadTypeCanBePending,
getContainingThreadID,
getCommunity,
getThreadListSearchResults,
reorderThreadSearchResults,
useAvailableThreadMemberActions,
threadMembersWithoutAddedAdmin,
patchThreadInfoToIncludeMentionedMembersOfParent,
useRoleMemberCountsForCommunity,
useRoleNamesToSpecialRole,
useRoleUserSurfacedPermissions,
getThreadsToDeleteText,
useOldestPrivateThreadInfo,
useUserProfileThreadInfo,
assertAllThreadInfosAreLegacy,
useOnScreenEntryEditableThreadInfos,
extractMentionedMembers,
isMemberActive,
createThreadTimestamps,
userHasDeviceList,
deviceListIsNonEmpty,
deviceListCanBeRequestedForUser,
expectedAccountDeletionUpdateTimeout,
};
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Nov 23, 1:01 AM (1 d, 19 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2553835
Default Alt Text
(74 KB)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment