Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F32134701
D15431.1765027087.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
17 KB
Referenced Files
None
Subscribers
None
D15431.1765027087.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D15431: [lib] introduce new role for Farcaster invitee and fetch invitees with conversation
Attached
Detach File
Event Timeline
Log In to Comment