Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3525649
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
54 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/shared/thread-utils.js b/lib/shared/thread-utils.js
index 3dcea8a52..0acf8b05b 100644
--- a/lib/shared/thread-utils.js
+++ b/lib/shared/thread-utils.js
@@ -1,1840 +1,1838 @@
// @flow
import invariant from 'invariant';
import _find from 'lodash/fp/find.js';
import _mapValues from 'lodash/fp/mapValues.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 { type ParserRules } from './markdown.js';
import { extractUserMentionsFromText } from './mention-utils.js';
import { getMessageTitle, isInvalidSidebarSource } from './message-utils.js';
import { relationshipBlockedInEitherDirection } from './relationship-utils.js';
import { useForwardLookupSearchText } from './search-utils.js';
import threadWatcher from './thread-watcher.js';
import {
fetchMostRecentMessagesActionTypes,
useFetchMostRecentMessages,
} from '../actions/message-actions.js';
import type { RemoveUsersFromThreadInput } from '../actions/thread-actions';
import {
newThreadActionTypes,
removeUsersFromThreadActionTypes,
} from '../actions/thread-actions.js';
import { searchUsers as searchUserCall } from '../actions/user-actions.js';
import ashoat from '../facts/ashoat.js';
import genesis from '../facts/genesis.js';
import { useLoggedInUserInfo } from '../hooks/account-hooks.js';
import {
hasPermission,
permissionsToBitmaskHex,
threadPermissionsFromBitmaskHex,
} from '../permissions/minimally-encoded-thread-permissions.js';
import {
permissionLookup,
getAllThreadPermissions,
makePermissionsBlob,
} from '../permissions/thread-permissions.js';
import type {
ChatThreadItem,
ChatMessageInfoItem,
} from '../selectors/chat-selectors.js';
import { useGlobalThreadSearchIndex } from '../selectors/nav-selectors.js';
import {
threadInfoSelector,
pendingToRealizedThreadIDsSelector,
threadInfosSelectorForThreadType,
} from '../selectors/thread-selectors.js';
import {
getRelativeMemberInfos,
usersWithPersonalThreadSelector,
} from '../selectors/user-selectors.js';
import type { CalendarQuery } from '../types/entry-types.js';
import { messageTypes } from '../types/message-types-enum.js';
import {
type RobotextMessageInfo,
type ComposableMessageInfo,
} from '../types/message-types.js';
import type {
RelativeMemberInfo,
RawThreadInfo,
MinimallyEncodedThreadCurrentUserInfo,
RoleInfo,
ThreadInfo,
MinimallyEncodedMemberInfo,
} from '../types/minimally-encoded-thread-permissions-types.js';
import {
decodeMinimallyEncodedRoleInfo,
minimallyEncodeMemberInfo,
minimallyEncodeRawThreadInfo,
minimallyEncodeRoleInfo,
minimallyEncodeThreadCurrentUserInfo,
} from '../types/minimally-encoded-thread-permissions-types.js';
import { userRelationshipStatus } from '../types/relationship-types.js';
import {
threadPermissionPropagationPrefixes,
threadPermissions,
configurableCommunityPermissions,
type ThreadPermission,
type ThreadPermissionsInfo,
type ThreadRolePermissionsBlob,
type UserSurfacedPermission,
threadPermissionFilterPrefixes,
} from '../types/thread-permission-types.js';
import {
type ThreadType,
threadTypes,
threadTypeIsCommunityRoot,
assertThreadType,
} from '../types/thread-types-enum.js';
import type {
LegacyRawThreadInfo,
LegacyRoleInfo,
ServerThreadInfo,
ServerMemberInfo,
ClientNewThreadRequest,
NewThreadResult,
ChangeThreadSettingsPayload,
UserProfileThreadInfo,
MixedRawThreadInfos,
LegacyMemberInfo,
} from '../types/thread-types.js';
import { updateTypes } from '../types/update-types-enum.js';
import { type ClientUpdateInfo } from '../types/update-types.js';
import type {
GlobalAccountUserInfo,
UserInfos,
AccountUserInfo,
LoggedInUserInfo,
UserInfo,
} from '../types/user-types.js';
import { useLegacyAshoatKeyserverCall } from '../utils/action-utils.js';
import type { GetENSNames } from '../utils/ens-helpers.js';
import {
ET,
entityTextToRawString,
getEntityTextAsString,
type ThreadEntity,
type UserEntity,
} from '../utils/entity-text.js';
import {
useDispatchActionPromise,
type DispatchActionPromise,
} from '../utils/redux-promise-utils.js';
import { useSelector } from '../utils/redux-utils.js';
import { firstLine } from '../utils/string-utils.js';
import { trimText } from '../utils/text-utils.js';
import { pendingThreadIDRegex } from '../utils/validation-utils.js';
function threadHasPermission(
threadInfo: ?(ThreadInfo | LegacyRawThreadInfo | RawThreadInfo),
permission: ThreadPermission,
): 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);
}
function viewerIsMember(
threadInfo: ?(ThreadInfo | LegacyRawThreadInfo | RawThreadInfo),
): boolean {
return !!(
threadInfo &&
threadInfo.currentUser.role !== null &&
threadInfo.currentUser.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 threadIsTopLevel(threadInfo: ?(RawThreadInfo | ThreadInfo)): boolean {
return threadInChatList(threadInfo) && threadIsChannel(threadInfo);
}
function threadIsChannel(threadInfo: ?(RawThreadInfo | ThreadInfo)): boolean {
return !!(threadInfo && threadInfo.type !== threadTypes.SIDEBAR);
}
function threadIsSidebar(threadInfo: ?(RawThreadInfo | ThreadInfo)): boolean {
return threadInfo?.type === threadTypes.SIDEBAR;
}
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 &&
threadInfo.type !== threadTypes.SIDEBAR
);
}
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<
- LegacyMemberInfo | MinimallyEncodedMemberInfo | RelativeMemberInfo,
- >,
+ memberInfos: $ReadOnlyArray<MinimallyEncodedMemberInfo | RelativeMemberInfo>,
): $ReadOnlyArray<string> {
return memberInfos
.filter(memberInfo => memberInfo.role)
.map(memberInfo => memberInfo.id);
}
function threadOtherMembers<
T: LegacyMemberInfo | MinimallyEncodedMemberInfo | RelativeMemberInfo,
>(memberInfos: $ReadOnlyArray<T>, viewerID: ?string): $ReadOnlyArray<T> {
return memberInfos.filter(
memberInfo => memberInfo.role && memberInfo.id !== viewerID,
);
}
function threadMembersWithoutAddedAshoat<
T: LegacyRawThreadInfo | RawThreadInfo | ThreadInfo,
>(threadInfo: T): $PropertyType<T, 'members'> {
if (threadInfo.community !== genesis.id) {
return threadInfo.members;
}
return threadInfo.members.filter(
member => member.id !== ashoat.id || member.role,
);
}
function threadIsGroupChat(threadInfo: ThreadInfo): boolean {
return threadInfo.members.length > 2;
}
function threadOrParentThreadIsGroupChat(
threadInfo: LegacyRawThreadInfo | RawThreadInfo | ThreadInfo,
) {
return threadMembersWithoutAddedAshoat(threadInfo).length > 2;
}
function threadIsPending(threadID: ?string): boolean {
return !!threadID?.startsWith('pending');
}
function threadIsPendingSidebar(threadID: ?string): boolean {
return !!threadID?.startsWith('pending/sidebar/');
}
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 {
const pendingThreadKey = sourceMessageID
? `sidebar/${sourceMessageID}`
: [...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('/');
const threadType =
threadTypeString === 'sidebar'
? threadTypes.SIDEBAR
: assertThreadType(Number(threadTypeString.replace('type', '')));
const memberIDs = threadTypeString === 'sidebar' ? [] : threadKey.split('+');
const sourceMessageID = threadTypeString === 'sidebar' ? 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,
});
const rawThreadInfo: RawThreadInfo = {
minimallyEncoded: true,
id: threadID,
type: threadType,
name: name ?? null,
description: null,
color: threadColor ?? generatePendingThreadColor(memberIDs),
creationTime: now,
parentThreadID: parentThreadInfo?.id ?? null,
containingThreadID: getContainingThreadID(parentThreadInfo, threadType),
community: getCommunity(parentThreadInfo),
members: members.map(member =>
minimallyEncodeMemberInfo({
id: member.id,
role: role.id,
permissions: membershipPermissions,
isSender: false,
}),
),
roles: {
[role.id]: role,
},
currentUser: minimallyEncodeThreadCurrentUserInfo({
role: role.id,
permissions: membershipPermissions,
subscription: {
pushNotifs: false,
home: false,
},
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 createPendingPersonalThread(
loggedInUserInfo: LoggedInUserInfo,
userID: string,
username: string,
): PendingPersonalThread {
const pendingPersonalThreadUserInfo = {
id: userID,
username: username,
};
const threadInfo = createPendingThread({
viewerID: loggedInUserInfo.id,
threadType: threadTypes.PERSONAL,
members: [loggedInUserInfo, pendingPersonalThreadUserInfo],
});
return { threadInfo, pendingPersonalThreadUserInfo };
}
function createPendingThreadItem(
loggedInUserInfo: LoggedInUserInfo,
user: UserIDAndUsername,
): ChatThreadItem {
const { threadInfo, pendingPersonalThreadUserInfo } =
createPendingPersonalThread(loggedInUserInfo, user.id, user.username);
return {
type: 'chatThreadItem',
threadInfo,
mostRecentMessageInfo: null,
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()];
}
type SharedCreatePendingSidebarInput = {
+sourceMessageInfo: ComposableMessageInfo | RobotextMessageInfo,
+parentThreadInfo: ThreadInfo,
+loggedInUserInfo: LoggedInUserInfo,
};
type BaseCreatePendingSidebarInput = {
...SharedCreatePendingSidebarInput,
+messageTitle: string,
};
function baseCreatePendingSidebar(
input: BaseCreatePendingSidebarInput,
): ThreadInfo {
const {
sourceMessageInfo,
parentThreadInfo,
loggedInUserInfo,
messageTitle,
} = input;
const { color, type: parentThreadType } = parentThreadInfo;
const threadName = trimText(messageTitle, 30);
const initialMembers = new Map<string, UserIDAndUsername>();
const { id: viewerID, username: viewerUsername } = loggedInUserInfo;
initialMembers.set(viewerID, { id: viewerID, username: viewerUsername });
if (userIsMember(parentThreadInfo, sourceMessageInfo.creator.id)) {
const { id: sourceAuthorID, username: sourceAuthorUsername } =
sourceMessageInfo.creator;
invariant(
sourceAuthorUsername,
'sourceAuthorUsername should be set in createPendingSidebar',
);
const initialMemberUserInfo = {
id: sourceAuthorID,
username: sourceAuthorUsername,
};
initialMembers.set(sourceAuthorID, initialMemberUserInfo);
}
const singleOtherUser = getSingleOtherUser(parentThreadInfo, viewerID);
if (parentThreadType === threadTypes.PERSONAL && singleOtherUser) {
const singleOtherUsername = parentThreadInfo.members.find(
member => member.id === singleOtherUser,
)?.username;
invariant(
singleOtherUsername,
'singleOtherUsername should be set in createPendingSidebar',
);
const singleOtherUserInfo = {
id: singleOtherUser,
username: singleOtherUsername,
};
initialMembers.set(singleOtherUser, singleOtherUserInfo);
}
if (sourceMessageInfo.type === messageTypes.TEXT) {
const mentionedMembersOfParent = extractMentionedMembers(
sourceMessageInfo.text,
parentThreadInfo,
);
for (const [memberID, member] of mentionedMembersOfParent) {
initialMembers.set(memberID, member);
}
}
return createPendingThread({
viewerID,
threadType: threadTypes.SIDEBAR,
members: [...initialMembers.values()],
parentThreadInfo,
threadColor: color,
name: threadName,
sourceMessageID: sourceMessageInfo.id,
});
}
// The message title here may have ETH addresses that aren't resolved to ENS
// names. This function should only be used in cases where we're sure that we
// don't care about the thread title. We should prefer createPendingSidebar
// wherever possible
type CreateUnresolvedPendingSidebarInput = {
...SharedCreatePendingSidebarInput,
+markdownRules: ParserRules,
};
function createUnresolvedPendingSidebar(
input: CreateUnresolvedPendingSidebarInput,
): ThreadInfo {
const {
sourceMessageInfo,
parentThreadInfo,
loggedInUserInfo,
markdownRules,
} = input;
const messageTitleEntityText = getMessageTitle(
sourceMessageInfo,
parentThreadInfo,
parentThreadInfo,
markdownRules,
);
const messageTitle = entityTextToRawString(messageTitleEntityText, {
ignoreViewer: true,
});
return baseCreatePendingSidebar({
sourceMessageInfo,
parentThreadInfo,
messageTitle,
loggedInUserInfo,
});
}
type CreatePendingSidebarInput = {
...SharedCreatePendingSidebarInput,
+markdownRules: ParserRules,
+getENSNames: ?GetENSNames,
};
async function createPendingSidebar(
input: CreatePendingSidebarInput,
): Promise<ThreadInfo> {
const {
sourceMessageInfo,
parentThreadInfo,
loggedInUserInfo,
markdownRules,
getENSNames,
} = input;
const messageTitleEntityText = getMessageTitle(
sourceMessageInfo,
parentThreadInfo,
parentThreadInfo,
markdownRules,
);
const messageTitle = await getEntityTextAsString(
messageTitleEntityText,
getENSNames,
{ ignoreViewer: true },
);
invariant(
messageTitle !== null && messageTitle !== undefined,
'getEntityTextAsString only returns falsey when passed falsey',
);
return baseCreatePendingSidebar({
sourceMessageInfo,
parentThreadInfo,
messageTitle,
loggedInUserInfo,
});
}
function pendingThreadType(numberOfOtherMembers: number): 4 | 6 | 7 {
if (numberOfOtherMembers === 0) {
return threadTypes.PRIVATE;
} else if (numberOfOtherMembers === 1) {
return threadTypes.PERSONAL;
} else {
return threadTypes.LOCAL;
}
}
function threadTypeCanBePending(threadType: ThreadType): boolean {
return (
threadType === threadTypes.PERSONAL ||
threadType === threadTypes.LOCAL ||
threadType === threadTypes.SIDEBAR ||
threadType === threadTypes.PRIVATE
);
}
type CreateRealThreadParameters = {
+threadInfo: ThreadInfo,
+dispatchActionPromise: DispatchActionPromise,
+createNewThread: ClientNewThreadRequest => Promise<NewThreadResult>,
+sourceMessageID: ?string,
+viewerID: ?string,
+handleError?: () => mixed,
+calendarQuery: CalendarQuery,
};
async function createRealThreadFromPendingThread({
threadInfo,
dispatchActionPromise,
createNewThread,
sourceMessageID,
viewerID,
calendarQuery,
}: CreateRealThreadParameters): Promise<string> {
if (!threadIsPending(threadInfo.id)) {
return threadInfo.id;
}
const otherMemberIDs = threadOtherMembers(threadInfo.members, viewerID).map(
member => member.id,
);
let resultPromise;
if (threadInfo.type !== threadTypes.SIDEBAR) {
invariant(
otherMemberIDs.length > 0,
'otherMemberIDs should not be empty for threads',
);
resultPromise = createNewThread({
type: pendingThreadType(otherMemberIDs.length),
initialMemberIDs: otherMemberIDs,
color: threadInfo.color,
calendarQuery,
});
} else {
invariant(
sourceMessageID,
'sourceMessageID should be set when creating a sidebar',
);
resultPromise = createNewThread({
type: threadTypes.SIDEBAR,
initialMemberIDs: otherMemberIDs,
color: threadInfo.color,
sourceMessageID,
parentThreadID: threadInfo.parentThreadID,
name: threadInfo.name,
calendarQuery,
});
}
void dispatchActionPromise(newThreadActionTypes, resultPromise);
const { newThreadID } = await resultPromise;
return newThreadID;
}
type RawThreadInfoOptions = {
+filterThreadEditAvatarPermission?: boolean,
+excludePinInfo?: boolean,
+filterManageInviteLinksPermission?: boolean,
+filterVoicedInAnnouncementChannelsPermission?: boolean,
+minimallyEncodePermissions?: boolean,
};
function rawThreadInfoFromServerThreadInfo(
serverThreadInfo: ServerThreadInfo,
viewerID: string,
options?: RawThreadInfoOptions,
): ?LegacyRawThreadInfo | ?RawThreadInfo {
const filterThreadEditAvatarPermission =
options?.filterThreadEditAvatarPermission;
const excludePinInfo = options?.excludePinInfo;
const filterManageInviteLinksPermission =
options?.filterManageInviteLinksPermission;
const filterVoicedInAnnouncementChannelsPermission =
options?.filterVoicedInAnnouncementChannelsPermission;
const shouldMinimallyEncodePermissions = options?.minimallyEncodePermissions;
const filterThreadPermissions = _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)),
);
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: {
home: false,
pushNotifs: false,
},
unread: null,
};
}
if (!permissionLookup(currentUserPermissions, threadPermissions.KNOW_OF)) {
return null;
}
const rolesWithFilteredThreadPermissions = _mapValues(role => ({
...role,
permissions: filterThreadPermissions(role.permissions),
}))(serverThreadInfo.roles);
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: rolesWithFilteredThreadPermissions,
currentUser,
repliesCount: serverThreadInfo.repliesCount,
containingThreadID: serverThreadInfo.containingThreadID,
community: serverThreadInfo.community,
};
const sourceMessageID = serverThreadInfo.sourceMessageID;
if (sourceMessageID) {
rawThreadInfo = { ...rawThreadInfo, sourceMessageID };
}
if (serverThreadInfo.avatar) {
rawThreadInfo = { ...rawThreadInfo, avatar: serverThreadInfo.avatar };
}
if (!excludePinInfo) {
rawThreadInfo = {
...rawThreadInfo,
pinnedCount: serverThreadInfo.pinnedCount,
};
}
return shouldMinimallyEncodePermissions
? minimallyEncodeRawThreadInfo(rawThreadInfo)
: rawThreadInfo;
}
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:
threadInfo.type === threadTypes.PRIVATE
? '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: getMinimallyEncodedCurrentUser(
rawThreadInfo,
viewerID,
userInfos,
),
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 (
rawThreadInfo.type === threadTypes.PERSONAL ||
rawThreadInfo.type === threadTypes.PRIVATE
) {
threadInfo = {
...threadInfo,
avatar: getUserAvatarForThread(rawThreadInfo, viewerID, userInfos),
};
}
if (pinnedCount) {
threadInfo = { ...threadInfo, pinnedCount };
}
return threadInfo;
}
function getMinimallyEncodedCurrentUser(
threadInfo: RawThreadInfo | ThreadInfo,
viewerID: ?string,
userInfos: UserInfos,
): MinimallyEncodedThreadCurrentUserInfo {
if (!threadFrozenDueToBlock(threadInfo, viewerID, userInfos)) {
return threadInfo.currentUser;
}
const decodedPermissions = threadPermissionsFromBitmaskHex(
threadInfo.currentUser.permissions,
);
const updatedPermissions = {
...decodedPermissions,
...disabledPermissions,
};
const encodedUpdatedPermissions = permissionsToBitmaskHex(updatedPermissions);
return {
...threadInfo.currentUser,
permissions: encodedUpdatedPermissions,
};
}
function threadIsWithBlockedUserOnly(
threadInfo: LegacyRawThreadInfo | RawThreadInfo | ThreadInfo,
viewerID: ?string,
userInfos: UserInfos,
checkOnlyViewerBlock?: boolean,
): boolean {
if (
threadOrParentThreadIsGroupChat(threadInfo) ||
threadOrParentThreadHasAdminRole(threadInfo)
) {
return false;
}
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 threadFrozenDueToBlock(
threadInfo: LegacyRawThreadInfo | RawThreadInfo | ThreadInfo,
viewerID: ?string,
userInfos: UserInfos,
): boolean {
return threadIsWithBlockedUserOnly(threadInfo, viewerID, userInfos);
}
function threadFrozenDueToViewerBlock(
threadInfo: RawThreadInfo | ThreadInfo,
viewerID: ?string,
userInfos: UserInfos,
): boolean {
return threadIsWithBlockedUserOnly(threadInfo, viewerID, userInfos, true);
}
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.',
};
// Since we don't have access to all of the ancestor ThreadInfos, we approximate
// "parent admin" as anybody with CHANGE_ROLE permissions.
function memberHasAdminPowers(
memberInfo:
| RelativeMemberInfo
| LegacyMemberInfo
| MinimallyEncodedMemberInfo
| ServerMemberInfo,
): boolean {
if (memberInfo.minimallyEncoded) {
return hasPermission(memberInfo.permissions, threadPermissions.CHANGE_ROLE);
}
return !!memberInfo.permissions[threadPermissions.CHANGE_ROLE]?.value;
}
function roleIsAdminRole(roleInfo: ?LegacyRoleInfo | ?RoleInfo): boolean {
return !!(roleInfo && !roleInfo.isDefault && roleInfo.name === 'Admins');
}
function threadHasAdminRole(
threadInfo: ?(
| LegacyRawThreadInfo
| RawThreadInfo
| ThreadInfo
| ServerThreadInfo
),
): boolean {
if (!threadInfo) {
return false;
}
return !!_find({ name: 'Admins' })(threadInfo.roles);
}
function threadOrParentThreadHasAdminRole(
threadInfo: LegacyRawThreadInfo | RawThreadInfo | ThreadInfo,
) {
return (
threadMembersWithoutAddedAshoat(threadInfo).filter(member =>
memberHasAdminPowers(member),
).length > 0
);
}
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 = [
threadPermissions.VOICED,
threadPermissions.EDIT_ENTRIES,
threadPermissions.EDIT_THREAD_NAME,
threadPermissions.EDIT_THREAD_COLOR,
threadPermissions.EDIT_THREAD_DESCRIPTION,
threadPermissions.CREATE_SUBCHANNELS,
threadPermissions.CREATE_SIDEBARS,
threadPermissions.JOIN_THREAD,
threadPermissions.EDIT_PERMISSIONS,
threadPermissions.ADD_MEMBERS,
threadPermissions.REMOVE_MEMBERS,
];
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 =
`Background 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 “Background” option in its settings.`;
function threadNoun(threadType: ThreadType, parentThreadID: ?string): string {
if (threadType === threadTypes.SIDEBAR) {
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.PERSONAL) {
return 'Personal';
} else if (threadType === threadTypes.SIDEBAR) {
return 'Thread';
} else if (threadType === threadTypes.PRIVATE) {
return 'Private';
} else if (
threadType === threadTypes.COMMUNITY_ROOT ||
threadType === threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT ||
threadType === threadTypes.GENESIS
) {
return 'Community';
} else {
return 'Secret';
}
}
function useWatchThread(threadInfo: ?ThreadInfo) {
const dispatchActionPromise = useDispatchActionPromise();
const callFetchMostRecentMessages = useFetchMostRecentMessages();
const threadID = threadInfo?.id;
const threadNotInChatList = !threadInChatList(threadInfo);
React.useEffect(() => {
if (threadID && threadNotInChatList) {
threadWatcher.watchID(threadID);
void dispatchActionPromise(
fetchMostRecentMessagesActionTypes,
callFetchMostRecentMessages({ threadID }),
);
}
return () => {
if (threadID && threadNotInChatList) {
threadWatcher.removeID(threadID);
}
};
}, [
callFetchMostRecentMessages,
dispatchActionPromise,
threadNotInChatList,
threadID,
]);
}
type ExistingThreadInfoFinderParams = {
+searching: boolean,
+userInfoInputArray: $ReadOnlyArray<AccountUserInfo>,
};
type ExistingThreadInfoFinder = (
params: ExistingThreadInfoFinderParams,
) => ?ThreadInfo;
function useExistingThreadInfoFinder(
baseThreadInfo: ?ThreadInfo,
): ExistingThreadInfoFinder {
const threadInfos = useSelector(threadInfoSelector);
const loggedInUserInfo = useLoggedInUserInfo();
const userInfos = useSelector(state => state.userStore.userInfos);
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;
const pendingThreadID = searching
? getPendingThreadID(
pendingThreadType(userInfoInputArray.length),
[...userInfoInputArray.map(user => user.id), viewerID],
sourceMessageID,
)
: getPendingThreadID(
baseThreadInfo.type,
baseThreadInfo.members.map(member => member.id),
sourceMessageID,
);
const realizedThreadID = pendingToRealizedThreadIDs.get(pendingThreadID);
if (realizedThreadID && threadInfos[realizedThreadID]) {
return threadInfos[realizedThreadID];
}
const updatedThread = searching
? createPendingThread({
viewerID,
threadType: pendingThreadType(userInfoInputArray.length),
members: [loggedInUserInfo, ...userInfoInputArray],
})
: baseThreadInfo;
return {
...updatedThread,
currentUser: getMinimallyEncodedCurrentUser(
updatedThread,
viewerID,
userInfos,
),
};
},
[
baseThreadInfo,
threadInfos,
loggedInUserInfo,
pendingToRealizedThreadIDs,
userInfos,
],
);
}
type ThreadTypeParentRequirement = 'optional' | 'required' | 'disabled';
function getThreadTypeParentRequirement(
threadType: ThreadType,
): ThreadTypeParentRequirement {
if (
threadType === threadTypes.COMMUNITY_OPEN_SUBTHREAD ||
threadType === threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD ||
//threadType === threadTypes.COMMUNITY_SECRET_SUBTHREAD ||
threadType === threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD ||
threadType === threadTypes.SIDEBAR
) {
return 'required';
} else if (
threadType === threadTypes.COMMUNITY_ROOT ||
threadType === threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT ||
threadType === threadTypes.GENESIS ||
threadType === threadTypes.PERSONAL ||
threadType === threadTypes.PRIVATE
) {
return 'disabled';
} else {
return 'optional';
}
}
function threadMemberHasPermission(
threadInfo:
| ServerThreadInfo
| LegacyRawThreadInfo
| RawThreadInfo
| ThreadInfo,
memberID: string,
permission: ThreadPermission,
): boolean {
for (const member of threadInfo.members) {
if (member.id !== memberID) {
continue;
}
if (member.minimallyEncoded) {
return hasPermission(member.permissions, permission);
}
return permissionLookup(member.permissions, permission);
}
return false;
}
function useCanCreateSidebarFromMessage(
threadInfo: ThreadInfo,
messageInfo: ComposableMessageInfo | RobotextMessageInfo,
): boolean {
const messageCreatorUserInfo = useSelector(
state => state.userStore.userInfos[messageInfo.creator.id],
);
if (
!messageInfo.id ||
threadInfo.sourceMessageID === messageInfo.id ||
isInvalidSidebarSource(messageInfo)
) {
return false;
}
const messageCreatorRelationship = messageCreatorUserInfo?.relationshipStatus;
const creatorRelationshipHasBlock =
messageCreatorRelationship &&
relationshipBlockedInEitherDirection(messageCreatorRelationship);
const hasCreateSidebarsPermission = threadHasPermission(
threadInfo,
threadPermissions.CREATE_SIDEBARS,
);
return hasCreateSidebarsPermission && !creatorRelationshipHasBlock;
}
function useSidebarExistsOrCanBeCreated(
threadInfo: ThreadInfo,
messageItem: ChatMessageInfoItem,
): boolean {
const canCreateSidebarFromMessage = useCanCreateSidebarFromMessage(
threadInfo,
messageItem.messageInfo,
);
return !!messageItem.threadCreatedFromMessage || canCreateSidebarFromMessage;
}
function checkIfDefaultMembersAreVoiced(threadInfo: ThreadInfo): boolean {
const defaultRoleID = Object.keys(threadInfo.roles).find(
roleID => threadInfo.roles[roleID].isDefault,
);
invariant(
defaultRoleID !== undefined,
'all threads should have a default role',
);
const defaultRole = threadInfo.roles[defaultRoleID];
const defaultRolePermissions = defaultRole.minimallyEncoded
? decodeMinimallyEncodedRoleInfo(defaultRole).permissions
: 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 (threadType === threadTypes.SIDEBAR) {
return parentThreadInfo.id;
}
if (!parentThreadInfo.containingThreadID) {
return parentThreadInfo.id;
}
return parentThreadInfo.containingThreadID;
}
function getCommunity(
parentThreadInfo:
| ?ServerThreadInfo
| LegacyRawThreadInfo
| RawThreadInfo
| ThreadInfo,
): ?string {
if (!parentThreadInfo) {
return null;
}
const { id, community, type } = parentThreadInfo;
if (community !== null && community !== undefined) {
return community;
}
if (threadTypeIsCommunityRoot(type)) {
return id;
}
return null;
}
function getThreadListSearchResults(
chatListData: $ReadOnlyArray<ChatThreadItem>,
searchText: string,
threadFilter: ThreadInfo => boolean,
threadSearchResults: $ReadOnlySet<string>,
usersSearchResults: $ReadOnlyArray<GlobalAccountUserInfo>,
loggedInUserInfo: ?LoggedInUserInfo,
): $ReadOnlyArray<ChatThreadItem> {
if (!searchText) {
return chatListData.filter(
item =>
threadIsTopLevel(item.threadInfo) && threadFilter(item.threadInfo),
);
}
const privateThreads = [];
const personalThreads = [];
const otherThreads = [];
for (const item of chatListData) {
if (!threadSearchResults.has(item.threadInfo.id)) {
continue;
}
if (item.threadInfo.type === threadTypes.PRIVATE) {
privateThreads.push({ ...item, sidebars: [] });
} else if (item.threadInfo.type === threadTypes.PERSONAL) {
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),
),
);
}
return chatItems;
}
type ThreadListSearchResult = {
+threadSearchResults: $ReadOnlySet<string>,
+usersSearchResults: $ReadOnlyArray<GlobalAccountUserInfo>,
};
function useThreadListSearch(
searchText: string,
viewerID: ?string,
): ThreadListSearchResult {
const callSearchUsers = useLegacyAshoatKeyserverCall(searchUserCall);
const usersWithPersonalThread = useSelector(usersWithPersonalThreadSelector);
const forwardLookupSearchText = useForwardLookupSearchText(searchText);
const searchUsers = React.useCallback(
async (usernamePrefix: string) => {
if (usernamePrefix.length === 0) {
return ([]: GlobalAccountUserInfo[]);
}
const { userInfos } = await callSearchUsers(usernamePrefix);
return userInfos.filter(
info => !usersWithPersonalThread.has(info.id) && info.id !== viewerID,
);
},
[callSearchUsers, usersWithPersonalThread, viewerID],
);
const [threadSearchResults, setThreadSearchResults] = React.useState(
new Set<string>(),
);
const [usersSearchResults, setUsersSearchResults] = React.useState<
$ReadOnlyArray<GlobalAccountUserInfo>,
>([]);
const threadSearchIndex = useGlobalThreadSearchIndex();
React.useEffect(() => {
void (async () => {
const results = threadSearchIndex.getSearchResults(searchText);
setThreadSearchResults(new Set<string>(results));
const usersResults = await searchUsers(forwardLookupSearchText);
setUsersSearchResults(usersResults);
})();
}, [searchText, forwardLookupSearchText, threadSearchIndex, searchUsers]);
return { threadSearchResults, usersSearchResults };
}
function removeMemberFromThread(
threadInfo: ThreadInfo,
memberInfo: RelativeMemberInfo,
dispatchActionPromise: DispatchActionPromise,
removeUserFromThreadServerCall: (
input: RemoveUsersFromThreadInput,
) => Promise<ChangeThreadSettingsPayload>,
) {
const customKeyName = `${removeUsersFromThreadActionTypes.started}:${memberInfo.id}`;
void dispatchActionPromise(
removeUsersFromThreadActionTypes,
removeUserFromThreadServerCall({
threadID: threadInfo.id,
memberIDs: [memberInfo.id],
}),
{ customKeyName },
);
}
function getAvailableThreadMemberActions(
memberInfo: RelativeMemberInfo,
threadInfo: ThreadInfo,
canEdit: ?boolean = true,
): $ReadOnlyArray<'change_role' | 'remove_user'> {
const role = memberInfo.role;
if (!canEdit || !role) {
return [];
}
const canRemoveMembers = threadHasPermission(
threadInfo,
threadPermissions.REMOVE_MEMBERS,
);
const canChangeRoles = threadHasPermission(
threadInfo,
threadPermissions.CHANGE_ROLE,
);
const result = [];
if (canChangeRoles && memberInfo.username && threadHasAdminRole(threadInfo)) {
result.push('change_role');
}
if (
canRemoveMembers &&
!memberInfo.isViewer &&
(canChangeRoles || threadInfo.roles[role]?.isDefault)
) {
result.push('remove_user');
}
return result;
}
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);
return createPendingThread({
viewerID,
threadType: threadTypes.SIDEBAR,
members,
parentThreadInfo,
threadColor: threadInfo.color,
name: threadInfo.name,
sourceMessageID: threadInfo.sourceMessageID,
});
}
function threadInfoInsideCommunity(
threadInfo: RawThreadInfo | ThreadInfo,
communityID: string,
): boolean {
return threadInfo.community === communityID || threadInfo.id === communityID;
}
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]);
}
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 = threadInfo.minimallyEncoded
? Object.keys(
decodeMinimallyEncodedRoleInfo(threadInfo.roles[roleID])
.permissions,
)
: Object.keys(threadInfo.roles[roleID].permissions);
const setOfUserSurfacedPermissions = new Set<UserSurfacedPermission>();
rolePermissions.forEach(rolePermission => {
const userSurfacedPermission = Object.keys(
configurableCommunityPermissions,
).find(key =>
configurableCommunityPermissions[key].has(rolePermission),
);
if (userSurfacedPermission) {
setOfUserSurfacedPermissions.add(userSurfacedPermission);
}
});
roleNamesToPermissions[roleName] = setOfUserSurfacedPermissions;
});
return roleNamesToPermissions;
}, [threadInfo]);
}
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)}`;
}
function useUserProfileThreadInfo(userInfo: ?UserInfo): ?UserProfileThreadInfo {
const userID = userInfo?.id;
const username = userInfo?.username;
const loggedInUserInfo = useLoggedInUserInfo();
const isViewerProfile = loggedInUserInfo?.id === userID;
const privateThreadInfosSelector = threadInfosSelectorForThreadType(
threadTypes.PRIVATE,
);
const privateThreadInfos = useSelector(privateThreadInfosSelector);
const personalThreadInfosSelector = threadInfosSelectorForThreadType(
threadTypes.PERSONAL,
);
const personalThreadInfos = useSelector(personalThreadInfosSelector);
const usersWithPersonalThread = useSelector(usersWithPersonalThreadSelector);
return React.useMemo(() => {
if (!loggedInUserInfo || !userID || !username) {
return null;
}
if (isViewerProfile) {
const privateThreadInfo: ?ThreadInfo = privateThreadInfos[0];
return privateThreadInfo ? { threadInfo: privateThreadInfo } : null;
}
if (usersWithPersonalThread.has(userID)) {
const personalThreadInfo: ?ThreadInfo = personalThreadInfos.find(
threadInfo =>
userID === getSingleOtherUser(threadInfo, loggedInUserInfo.id),
);
return personalThreadInfo ? { threadInfo: personalThreadInfo } : null;
}
const pendingPersonalThreadInfo = createPendingPersonalThread(
loggedInUserInfo,
userID,
username,
);
return pendingPersonalThreadInfo;
}, [
isViewerProfile,
loggedInUserInfo,
personalThreadInfos,
privateThreadInfos,
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];
}),
);
}
export {
threadHasPermission,
viewerIsMember,
threadInChatList,
threadIsTopLevel,
threadIsChannel,
threadIsSidebar,
threadInBackgroundChatList,
threadInHomeChatList,
threadIsInHome,
threadInFilterList,
userIsMember,
threadActualMembers,
threadOtherMembers,
threadIsGroupChat,
threadIsPending,
threadIsPendingSidebar,
getSingleOtherUser,
getPendingThreadID,
parsePendingThreadID,
createPendingThread,
createUnresolvedPendingSidebar,
extractNewMentionedParentMembers,
createPendingSidebar,
pendingThreadType,
createRealThreadFromPendingThread,
getMinimallyEncodedCurrentUser,
threadFrozenDueToBlock,
threadFrozenDueToViewerBlock,
rawThreadInfoFromServerThreadInfo,
threadUIName,
threadInfoFromRawThreadInfo,
threadTypeDescriptions,
memberHasAdminPowers,
roleIsAdminRole,
threadHasAdminRole,
identifyInvalidatedThreads,
permissionsDisabledByBlock,
emptyItemText,
threadNoun,
threadLabel,
useWatchThread,
useExistingThreadInfoFinder,
getThreadTypeParentRequirement,
threadMemberHasPermission,
useCanCreateSidebarFromMessage,
useSidebarExistsOrCanBeCreated,
checkIfDefaultMembersAreVoiced,
draftKeySuffix,
draftKeyFromThreadID,
threadTypeCanBePending,
getContainingThreadID,
getCommunity,
getThreadListSearchResults,
useThreadListSearch,
removeMemberFromThread,
getAvailableThreadMemberActions,
threadMembersWithoutAddedAshoat,
patchThreadInfoToIncludeMentionedMembersOfParent,
threadInfoInsideCommunity,
useRoleMemberCountsForCommunity,
useRoleUserSurfacedPermissions,
getThreadsToDeleteText,
useUserProfileThreadInfo,
assertAllThreadInfosAreLegacy,
};
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Wed, Dec 25, 5:58 PM (6 h, 18 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2700645
Default Alt Text
(54 KB)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment