Page MenuHomePhorge

D15431.1765027087.diff
No OneTemporary

Size
17 KB
Referenced Files
None
Subscribers
None

D15431.1765027087.diff

diff --git a/lib/permissions/special-roles.js b/lib/permissions/special-roles.js
--- a/lib/permissions/special-roles.js
+++ b/lib/permissions/special-roles.js
@@ -15,6 +15,7 @@
export const specialRoles = Object.freeze({
DEFAULT_ROLE: 1,
ADMIN_ROLE: 2,
+ INVITEE_ROLE: 3,
});
export type SpecialRole = $Values<typeof specialRoles>;
@@ -25,6 +26,7 @@
export const defaultSpecialRoles = Object.freeze({
Members: specialRoles.DEFAULT_ROLE,
Admins: specialRoles.ADMIN_ROLE,
+ Invitees: specialRoles.INVITEE_ROLE,
});
function patchRoleInfoWithSpecialRole(role: RoleInfo): RoleInfo {
diff --git a/lib/shared/farcaster/farcaster-api.js b/lib/shared/farcaster/farcaster-api.js
--- a/lib/shared/farcaster/farcaster-api.js
+++ b/lib/shared/farcaster/farcaster-api.js
@@ -6,9 +6,11 @@
import type {
FarcasterConversation,
+ FarcasterConversationInvitee,
FarcasterInboxConversation,
} from './farcaster-conversation-types.js';
import {
+ farcasterConversationInviteeValidator,
farcasterConversationValidator,
farcasterInboxConversationValidator,
} from './farcaster-conversation-types.js';
@@ -276,6 +278,57 @@
);
}
+export type FetchFarcasterConversationInvitesInput = {
+ +conversationId: string,
+};
+
+type FetchFarcasterConversationInvitesResultData = {
+ +invites: Array<FarcasterConversationInvitee>,
+ ...
+};
+const fetchFarcasterConversationInvitesResultDataValidator: TInterface<FetchFarcasterConversationInvitesResultData> =
+ tShapeInexact({
+ invites: t.list(farcasterConversationInviteeValidator),
+ });
+
+export type FetchFarcasterConversationInvitesResult = {
+ +result: FetchFarcasterConversationInvitesResultData,
+ ...
+};
+const fetchFarcasterConversationInvitesResultValidator: TInterface<FetchFarcasterConversationInvitesResult> =
+ tShapeInexact({
+ result: fetchFarcasterConversationInvitesResultDataValidator,
+ });
+
+function useFetchFarcasterConversationInvites(): (
+ input: FetchFarcasterConversationInvitesInput,
+) => Promise<FetchFarcasterConversationInvitesResult> {
+ const { sendFarcasterRequest } = useTunnelbroker();
+ return React.useCallback(
+ async (input: FetchFarcasterConversationInvitesInput) => {
+ const { conversationId } = input;
+ const params = new URLSearchParams({
+ conversationId,
+ });
+
+ const response = await sendFarcasterRequest({
+ apiVersion: 'v2',
+ endpoint: 'direct-cast-group-invites',
+ method: { type: 'GET' },
+ payload: params.toString(),
+ });
+ const parsedResult = JSON.parse(response);
+ const result: FetchFarcasterConversationInvitesResult =
+ assertWithValidator(
+ parsedResult,
+ fetchFarcasterConversationInvitesResultValidator,
+ );
+ return result;
+ },
+ [sendFarcasterRequest],
+ );
+}
+
export type UpdateFarcasterGroupNameAndDescriptionInput = {
+conversationId: string,
+name: string,
@@ -601,4 +654,5 @@
useModifyFarcasterMembershipInput,
useSendFarcasterReaction,
useAcceptInvite,
+ useFetchFarcasterConversationInvites,
};
diff --git a/lib/shared/farcaster/farcaster-conversation-types.js b/lib/shared/farcaster/farcaster-conversation-types.js
--- a/lib/shared/farcaster/farcaster-conversation-types.js
+++ b/lib/shared/farcaster/farcaster-conversation-types.js
@@ -183,4 +183,23 @@
viewerContext: farcasterConversationViewerContextValidator,
});
-export { farcasterInboxConversationValidator, farcasterConversationValidator };
+export type FarcasterConversationInvitee = {
+ +inviter?: FarcasterDCUserBase,
+ +invitee: FarcasterDCUserBase,
+ +inviteTimestamp: number,
+ +role?: 'member' | 'admin',
+ ...
+};
+const farcasterConversationInviteeValidator: TInterface<FarcasterConversationInvitee> =
+ tShapeInexact({
+ inviter: t.maybe(FarcasterDCUserBaseValidator),
+ invitee: FarcasterDCUserBaseValidator,
+ inviteTimestamp: t.Number,
+ role: t.maybe(t.enums.of(['member', 'admin'])),
+ });
+
+export {
+ farcasterInboxConversationValidator,
+ farcasterConversationValidator,
+ farcasterConversationInviteeValidator,
+};
diff --git a/lib/shared/farcaster/farcaster-hooks.js b/lib/shared/farcaster/farcaster-hooks.js
--- a/lib/shared/farcaster/farcaster-hooks.js
+++ b/lib/shared/farcaster/farcaster-hooks.js
@@ -9,8 +9,12 @@
type ProcessFarcasterOpsPayload,
} from './farcaster-actions.js';
import {
+ type FetchFarcasterConversationInput,
+ type FetchFarcasterConversationInvitesInput,
+ type FetchFarcasterConversationInvitesResult,
type FetchFarcasterConversationResult,
useFetchFarcasterConversation,
+ useFetchFarcasterConversationInvites,
useFetchFarcasterInbox,
useFetchFarcasterMessages,
} from './farcaster-api.js';
@@ -65,6 +69,7 @@
import {
conversationIDFromFarcasterThreadID,
farcasterThreadIDFromConversationID,
+ isFarcasterPersonalConversationID,
userIDFromFID,
} from '../id-utils.js';
import { threadSpecs } from '../threads/thread-specs.js';
@@ -308,13 +313,16 @@
};
async function fetchAndProcessConversation(
conversationID: string,
- fetchFarcasterConversation: (input: {
- conversationId: string,
- }) => Promise<?FetchFarcasterConversationResult>,
+ fetchFarcasterConversation: (
+ input: FetchFarcasterConversationInput,
+ ) => Promise<FetchFarcasterConversationResult>,
+ fetchFarcasterConversationInvites: (
+ input: FetchFarcasterConversationInvitesInput,
+ ) => Promise<FetchFarcasterConversationInvitesResult>,
fetchUsersByFIDs: GetCommFCUsersForFIDs,
addLog?: AddLogCallback,
): Promise<?ConversationFetchResult> {
- const conversationResult = await withRetry(
+ const conversationResultPromise = withRetry(
() =>
fetchFarcasterConversation({
conversationId: conversationID,
@@ -325,6 +333,46 @@
`fetchConversation(${conversationID})`,
);
+ const fetchFarcasterInvites = async () => {
+ try {
+ return await fetchFarcasterConversationInvites({
+ conversationId: conversationID,
+ });
+ } catch (error) {
+ const messageForException = getMessageForException(error);
+ if (
+ messageForException?.includes('Not authorized to view pending invites')
+ ) {
+ // This is normal for many Farcaster group conversations
+ return null;
+ }
+ throw error;
+ }
+ };
+
+ const conversationInviteesResultPromise: Promise<?FetchFarcasterConversationInvitesResult> =
+ (async () => {
+ if (isFarcasterPersonalConversationID(conversationID)) {
+ // Personal conversations don't have invites
+ return null;
+ }
+ try {
+ return await withRetry(
+ fetchFarcasterInvites,
+ MAX_RETRIES,
+ RETRY_DELAY_MS,
+ );
+ } catch (error) {
+ // Errors in fetching invites should not fail the entire conversation
+ return null;
+ }
+ })();
+
+ const [conversationResult, conversationInviteesResult] = await Promise.all([
+ conversationResultPromise,
+ conversationInviteesResultPromise,
+ ]);
+
if (!conversationResult) {
if (addLog) {
addLog(
@@ -352,7 +400,10 @@
}
const farcasterConversation = conversationResult.result.conversation;
- let thread = createFarcasterRawThreadInfo(farcasterConversation);
+ let thread = createFarcasterRawThreadInfo(
+ farcasterConversation,
+ conversationInviteesResult?.result?.invites ?? [],
+ );
const fids = thread.members.map(member => member.id);
if (fids.length === 0 && addLog) {
@@ -392,7 +443,11 @@
members: threadMembers,
};
- return { farcasterConversation, thread, threadMembers };
+ return {
+ farcasterConversation,
+ thread,
+ threadMembers,
+ };
}
function useFetchConversationWithBatching(): (
@@ -401,6 +456,8 @@
) => Promise<?FarcasterConversation> {
const fetchUsersByFIDs = useGetCommFCUsersForFIDs();
const fetchFarcasterConversation = useFetchFarcasterConversation();
+ const fetchFarcasterConversationInvites =
+ useFetchFarcasterConversationInvites();
const { addLog } = useDebugLogs();
return React.useCallback(
@@ -412,6 +469,7 @@
const result = await fetchAndProcessConversation(
conversationID,
fetchFarcasterConversation,
+ fetchFarcasterConversationInvites,
fetchUsersByFIDs,
addLog,
);
@@ -461,7 +519,12 @@
return null;
}
},
- [addLog, fetchFarcasterConversation, fetchUsersByFIDs],
+ [
+ addLog,
+ fetchFarcasterConversation,
+ fetchFarcasterConversationInvites,
+ fetchUsersByFIDs,
+ ],
);
}
@@ -472,6 +535,8 @@
) => Promise<?FarcasterConversation> {
const fetchUsersByFIDs = useGetCommFCUsersForFIDs();
const fetchFarcasterConversation = useFetchFarcasterConversation();
+ const fetchFarcasterConversationInvites =
+ useFetchFarcasterConversationInvites();
const fetchFarcasterMessages = useFetchMessagesForConversation();
const { addLog } = useDebugLogs();
@@ -485,6 +550,7 @@
const result = await fetchAndProcessConversation(
conversationID,
fetchFarcasterConversation,
+ fetchFarcasterConversationInvites,
fetchUsersByFIDs,
addLog,
);
@@ -567,6 +633,7 @@
addLog,
fetchFarcasterConversation,
fetchFarcasterMessages,
+ fetchFarcasterConversationInvites,
fetchUsersByFIDs,
],
);
diff --git a/lib/shared/farcaster/farcaster-user-types.js b/lib/shared/farcaster/farcaster-user-types.js
--- a/lib/shared/farcaster/farcaster-user-types.js
+++ b/lib/shared/farcaster/farcaster-user-types.js
@@ -46,34 +46,11 @@
pfp: t.maybe(farcasterProfilePictureValidator),
});
-type FarcasterDCUserViewerContext = {
- +canSendDirectCasts?: boolean,
- +canAddToGroupDirectly?: boolean,
- +nerfed?: boolean,
- +invisible?: boolean,
- +blocking?: boolean,
- +blockedBy?: boolean,
- +enableNotifications?: boolean,
- ...
-};
-const farcasterDCUserViewerContextValidator: TInterface<FarcasterDCUserViewerContext> =
- tShapeInexact({
- canSendDirectCasts: t.maybe(t.Boolean),
- canAddToGroupDirectly: t.maybe(t.Boolean),
- nerfed: t.maybe(t.Boolean),
- invisible: t.maybe(t.Boolean),
- blocking: t.maybe(t.Boolean),
- blockedBy: t.maybe(t.Boolean),
- enableNotifications: t.maybe(t.Boolean),
- });
-
export type FarcasterDCUser = {
+fid: number,
+username?: string,
+displayName: string,
+pfp?: FarcasterProfilePicture,
- +referrerUsername?: string,
- +viewerContext?: FarcasterDCUserViewerContext,
...
};
const farcasterDCUserValidator: TInterface<FarcasterDCUser> = tShapeInexact({
@@ -81,8 +58,6 @@
username: t.maybe(t.String),
displayName: t.String,
pfp: t.maybe(farcasterProfilePictureValidator),
- referrerUsername: t.maybe(t.String),
- viewerContext: t.maybe(farcasterDCUserViewerContextValidator),
});
export {
diff --git a/lib/shared/id-utils.js b/lib/shared/id-utils.js
--- a/lib/shared/id-utils.js
+++ b/lib/shared/id-utils.js
@@ -149,6 +149,12 @@
return threadID.replace(farcasterIDPrefix, '');
}
+const farcasterPersonalConversationIDRegExp: RegExp = /^\d+-\d+$/;
+
+function isFarcasterPersonalConversationID(conversationID: string): boolean {
+ return farcasterPersonalConversationIDRegExp.test(conversationID);
+}
+
const farcasterUserIDPrefix = 'FID#';
function userIDFromFID(farcasterID: string): string {
@@ -192,6 +198,7 @@
farcasterIDPrefix,
farcasterThreadIDFromConversationID,
conversationIDFromFarcasterThreadID,
+ isFarcasterPersonalConversationID,
userIDFromFID,
extractFIDFromUserID,
farcasterPersonalConversationID,
diff --git a/lib/shared/thread-utils.js b/lib/shared/thread-utils.js
--- a/lib/shared/thread-utils.js
+++ b/lib/shared/thread-utils.js
@@ -1062,6 +1062,15 @@
return !!(roleInfo && !roleInfo.isDefault && roleInfo.name === 'Admins');
}
+function roleIsInviteeRole(
+ roleInfo: ?ClientLegacyRoleInfo | ?RoleInfo,
+): boolean {
+ if (roleInfo?.specialRole === specialRoles.INVITEE_ROLE) {
+ return true;
+ }
+ return !!(roleInfo && !roleInfo.isDefault && roleInfo.name === 'Invitees');
+}
+
function threadHasAdminRole(
threadInfo: ?(
| LegacyRawThreadInfo
@@ -1452,7 +1461,7 @@
return React.useMemo(() => {
const { role } = memberInfo;
- if (!canEdit || !role) {
+ if (!canEdit || !role || roleIsInviteeRole(threadInfo.roles[role])) {
return [];
}
@@ -1858,4 +1867,5 @@
deviceListIsNonEmpty,
deviceListCanBeRequestedForUser,
expectedAccountDeletionUpdateTimeout,
+ roleIsInviteeRole,
};
diff --git a/lib/utils/create-farcaster-raw-thread-info.js b/lib/utils/create-farcaster-raw-thread-info.js
--- a/lib/utils/create-farcaster-raw-thread-info.js
+++ b/lib/utils/create-farcaster-raw-thread-info.js
@@ -15,6 +15,7 @@
import type {
FarcasterConversation,
FarcasterInboxConversation,
+ FarcasterConversationInvitee,
} from '../shared/farcaster/farcaster-conversation-types.js';
import { farcasterThreadIDFromConversationID } from '../shared/id-utils.js';
import { stringForUserExplicit } from '../shared/user-utils.js';
@@ -56,6 +57,7 @@
},
+memberIDs: $ReadOnlyArray<string>,
+adminIDs: $ReadOnlySet<string>,
+ +inviteeIDs: $ReadOnlySet<string>,
+viewerAccess: 'read' | 'read-write' | 'admin',
+muted: boolean,
+unread: boolean,
@@ -75,6 +77,7 @@
},
+memberIDs: $ReadOnlyArray<string>,
+adminIDs: $ReadOnlySet<string>,
+ +inviteeIDs: $ReadOnlySet<string>,
+currentUserOptions: {
+isAdmin: boolean,
+unread: boolean,
@@ -88,9 +91,19 @@
permissionBlobs,
memberIDs,
adminIDs,
+ inviteeIDs,
currentUserOptions,
threadType,
} = options;
+ const inviteeRole: RoleInfo = {
+ ...minimallyEncodeRoleInfo({
+ id: `${threadID}/invitee/role`,
+ name: 'Invitees',
+ permissions: permissionBlobs.Members,
+ isDefault: false,
+ }),
+ specialRole: specialRoles.INVITEE_ROLE,
+ };
const membersRole: RoleInfo = {
...minimallyEncodeRoleInfo({
id: `${threadID}/member/role`,
@@ -117,16 +130,27 @@
if (adminsRole) {
roles[adminsRole.id] = adminsRole;
}
+ if (threadType === farcasterThreadTypes.FARCASTER_GROUP) {
+ roles[inviteeRole.id] = inviteeRole;
+ }
- const members = memberIDs.map(fid => ({
- id: fid,
- // This flag was introduced for sidebars to show who replied to a thread.
- // Now it doesn't seem to be used anywhere. Regardless, for Farcaster
- // threads its value doesn't matter.
- isSender: true,
- minimallyEncoded: true,
- role: adminIDs.has(fid) && adminsRole ? adminsRole.id : membersRole.id,
- }));
+ const members = memberIDs.map(fid => {
+ let role = membersRole.id;
+ if (inviteeIDs.has(fid)) {
+ role = inviteeRole.id;
+ } else if (adminIDs.has(fid) && adminsRole) {
+ role = adminsRole.id;
+ }
+ return {
+ id: fid,
+ // This flag was introduced for sidebars to show who replied to a thread.
+ // Now it doesn't seem to be used anywhere. Regardless, for Farcaster
+ // threads its value doesn't matter.
+ isSender: true,
+ minimallyEncoded: true,
+ role,
+ };
+ });
let currentUserRole = null;
let permissionsBlob;
@@ -169,6 +193,7 @@
permissionBlobs,
memberIDs: threadData.memberIDs,
adminIDs: threadData.adminIDs,
+ inviteeIDs: threadData.inviteeIDs,
currentUserOptions: {
isAdmin: threadData.viewerAccess === 'admin',
unread: threadData.unread,
@@ -200,15 +225,19 @@
function createFarcasterRawThreadInfo(
conversation: FarcasterConversation,
+ invitees: $ReadOnlyArray<FarcasterConversationInvitee>,
): FarcasterRawThreadInfo {
const threadID = farcasterThreadIDFromConversationID(
conversation.conversationId,
);
const removedUsers = new Set(conversation.removedFids);
- const memberIDs = conversation.participants
+ const invitedUsers = invitees.map(invitee => invitee.invitee);
+ const allParticipants = [...conversation.participants, ...invitedUsers];
+ const memberIDs = allParticipants
.filter(p => !removedUsers.has(p.fid))
.map(p => `${p.fid}`);
const adminIDs = new Set(conversation.adminFids.map(fid => `${fid}`));
+ const inviteeIDs = new Set(invitees.map(invitee => `${invitee.invitee.fid}`));
const unread =
conversation.unreadCount > 0 ||
conversation.viewerContext.manuallyMarkedUnread;
@@ -240,6 +269,7 @@
getFarcasterRolePermissionsBlobsFromConversation(conversation),
memberIDs,
adminIDs,
+ inviteeIDs,
viewerAccess: conversation.viewerContext.access,
muted: conversation.viewerContext.muted,
unread,
@@ -273,6 +303,7 @@
permissionBlobs,
memberIDs,
adminIDs: new Set(),
+ inviteeIDs: new Set(),
viewerAccess: 'read-write',
muted: false,
unread: false,
@@ -338,6 +369,7 @@
const threadAdminUserIDs = threadInfo.members
.filter(member => member.role === adminsRole?.id)
.map(member => member.id);
+ //TODO: update this condition and compute proper `inviteeIDs`
if (
adminsRole &&
(adminIDs.size !== threadAdminUserIDs.length ||
@@ -353,6 +385,7 @@
permissionBlobs,
memberIDs: threadInfo.members.map(member => member.id),
adminIDs,
+ inviteeIDs: new Set(),
currentUserOptions: {
isAdmin: adminIDs.has(currentUserFID),
unread: conversationIsUnread,

File Metadata

Mime Type
text/plain
Expires
Sat, Dec 6, 1:18 PM (16 h, 16 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5837896
Default Alt Text
D15431.1765027087.diff (17 KB)

Event Timeline