Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F32171713
D15375.1765060982.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
22 KB
Referenced Files
None
Subscribers
None
D15375.1765060982.diff
View Options
diff --git a/lib/reducers/message-reducer.js b/lib/reducers/message-reducer.js
--- a/lib/reducers/message-reducer.js
+++ b/lib/reducers/message-reducer.js
@@ -1942,12 +1942,17 @@
]),
};
} else if (action.type === processFarcasterOpsActionType) {
- const { rawMessageInfos, updateInfos, additionalMessageInfos } =
- action.payload;
+ const {
+ rawMessageInfos,
+ updateInfos,
+ additionalMessageInfos,
+ truncationStatuses,
+ } = action.payload;
const messagesResult = mergeUpdatesWithMessageInfos(
rawMessageInfos,
updateInfos,
+ truncationStatuses,
);
const { messageStoreOperations, messageStore: newMessageStore } =
diff --git a/lib/shared/farcaster/farcaster-actions.js b/lib/shared/farcaster/farcaster-actions.js
--- a/lib/shared/farcaster/farcaster-actions.js
+++ b/lib/shared/farcaster/farcaster-actions.js
@@ -1,6 +1,9 @@
// @flow
-import type { RawMessageInfo } from '../../types/message-types.js';
+import type {
+ MessageTruncationStatuses,
+ RawMessageInfo,
+} from '../../types/message-types.js';
import type { ClientUpdateInfo } from '../../types/update-types.js';
export const processFarcasterOpsActionType = 'PROCESS_FARCASTER_OPS';
@@ -10,4 +13,5 @@
+additionalMessageInfos?: $ReadOnlyArray<RawMessageInfo>,
+updateInfos: $ReadOnlyArray<ClientUpdateInfo>,
+userIDs?: $ReadOnlyArray<string>,
+ +truncationStatuses?: MessageTruncationStatuses,
};
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
@@ -14,6 +14,7 @@
useFetchFarcasterMessages,
} from './farcaster-api.js';
import type { FarcasterConversation } from './farcaster-conversation-types.js';
+import { useFarcasterMessageFetching } from './farcaster-message-fetching-context.js';
import {
type FarcasterMessage,
farcasterMessageValidator,
@@ -33,6 +34,7 @@
FarcasterRawThreadInfo,
} from '../../types/minimally-encoded-thread-permissions-types';
import type { Dispatch } from '../../types/redux-types.js';
+import type { ThreadType } from '../../types/thread-types-enum.js';
import { updateTypes } from '../../types/update-types-enum.js';
import type { ClientUpdateInfo } from '../../types/update-types.js';
import { extractFarcasterIDsFromPayload } from '../../utils/conversion-utils.js';
@@ -51,6 +53,7 @@
farcasterThreadIDFromConversationID,
userIDFromFID,
} from '../id-utils.js';
+import { threadSpecs } from '../threads/thread-specs.js';
const FARCASTER_DATA_BATCH_SIZE = 20;
const MAX_RETRIES = 3;
@@ -375,9 +378,11 @@
function useFetchMessagesForConversation(): (
conversationID: string,
messagesNumberLimit?: number,
+ cursor?: ?string,
) => Promise<{
+messages: Array<RawMessageInfo>,
+userIDs: Array<string>,
+ +newCursor?: ?string,
}> {
const fetchFarcasterMessages = useFetchFarcasterMessages();
const fetchUsersByFIDs = useGetCommFCUsersForFIDs();
@@ -386,14 +391,15 @@
async (
conversationID: string,
messagesNumberLimit: number = 20,
+ cursor?: ?string,
): Promise<{
+messages: Array<RawMessageInfo>,
+userIDs: Array<string>,
+ +newCursor?: ?string,
}> => {
const result: Array<RawMessageInfo> = [];
const userIDs: Array<string> = [];
try {
- let cursor: ?string = null;
let totalMessagesFetched = 0;
do {
@@ -454,6 +460,7 @@
return {
messages: result,
userIDs: Array.from(new Set(userIDs)),
+ newCursor: cursor,
};
},
[fetchFarcasterMessages, fetchUsersByFIDs],
@@ -798,6 +805,42 @@
return { inProgress, progress };
}
+function useFarcasterThreadRefresher(
+ activeChatThreadID: ?string,
+ threadType: ?ThreadType,
+ appFocused: boolean,
+): void {
+ const prevActiveThreadID = React.useRef<?string>(null);
+ const prevAppFocused = React.useRef(appFocused);
+ const farcasterRefreshConversation = useRefreshFarcasterConversation();
+ const farcasterMessageFetching = useFarcasterMessageFetching();
+
+ React.useEffect(() => {
+ if (
+ threadType &&
+ activeChatThreadID &&
+ (prevActiveThreadID.current !== activeChatThreadID ||
+ (appFocused && !prevAppFocused.current))
+ ) {
+ threadSpecs[threadType].protocol().onOpenThread?.(
+ { threadID: activeChatThreadID },
+ {
+ farcasterRefreshConversation,
+ farcasterMessageFetching,
+ },
+ );
+ }
+ prevActiveThreadID.current = activeChatThreadID;
+ prevAppFocused.current = appFocused;
+ }, [
+ activeChatThreadID,
+ appFocused,
+ farcasterMessageFetching,
+ farcasterRefreshConversation,
+ threadType,
+ ]);
+}
+
export {
useFarcasterConversationsSync,
useFetchConversationWithBatching,
@@ -807,4 +850,5 @@
useRefreshFarcasterConversation,
useAddNewFarcasterMessage,
useFarcasterSync,
+ useFarcasterThreadRefresher,
};
diff --git a/lib/shared/farcaster/farcaster-message-fetching-context.js b/lib/shared/farcaster/farcaster-message-fetching-context.js
new file mode 100644
--- /dev/null
+++ b/lib/shared/farcaster/farcaster-message-fetching-context.js
@@ -0,0 +1,217 @@
+// @flow
+
+import invariant from 'invariant';
+import * as React from 'react';
+import { useDispatch } from 'react-redux';
+
+import { processFarcasterOpsActionType } from './farcaster-actions.js';
+import { useFetchMessagesForConversation } from './farcaster-hooks.js';
+import {
+ type MessageTruncationStatus,
+ messageTruncationStatus,
+ type RawMessageInfo,
+} from '../../types/message-types.js';
+import type { ClientUpdateInfo } from '../../types/update-types.js';
+import { conversationIDFromFarcasterThreadID } from '../id-utils.js';
+import { fetchMessagesFromDB } from '../message-utils.js';
+
+type ThreadFetchingState = {
+ +farcasterCursor: ?string,
+ +dbExhausted: boolean,
+ +farcasterExhausted: boolean,
+};
+
+export type FarcasterMessageFetchingContextType = {
+ +fetchMoreMessages: (
+ threadID: string,
+ numMessages: number,
+ currentOffset: number,
+ ) => Promise<void>,
+};
+
+const FarcasterMessageFetchingContext: React.Context<?FarcasterMessageFetchingContextType> =
+ React.createContext<?FarcasterMessageFetchingContextType>(null);
+
+type ProcessResultsInput = {
+ +dbResult: ?{
+ +threadID: string,
+ +rawMessageInfos: $ReadOnlyArray<RawMessageInfo>,
+ +truncationStatus: MessageTruncationStatus,
+ },
+ +farcasterResult: ?{
+ +messages: $ReadOnlyArray<RawMessageInfo>,
+ +newCursor: ?string,
+ +userIDs: $ReadOnlyArray<string>,
+ },
+};
+
+type ProcessResultsOutput = {
+ +combinedMessages: $ReadOnlyArray<RawMessageInfo>,
+ +newCursor: ?string,
+ +bothExhausted: boolean,
+ +userIDs: $ReadOnlyArray<string>,
+};
+
+function processResults(input: ProcessResultsInput): ProcessResultsOutput {
+ const { dbResult, farcasterResult } = input;
+
+ const dbMessages = dbResult?.rawMessageInfos ?? [];
+ const farcasterMessages = farcasterResult?.messages ?? [];
+
+ // Deduplicate messages, keeping Farcaster messages over DB messages
+ const messageMap = new Map<string, RawMessageInfo>();
+
+ // First add DB messages
+ for (const message of dbMessages) {
+ if (message.id) {
+ messageMap.set(message.id, message);
+ }
+ }
+
+ for (const message of farcasterMessages) {
+ if (message.id) {
+ messageMap.set(message.id, message);
+ }
+ }
+
+ const combinedMessages = Array.from(messageMap.values());
+
+ const newCursor = farcasterResult?.newCursor ?? null;
+ const dbExhausted =
+ dbResult?.truncationStatus === messageTruncationStatus.EXHAUSTIVE;
+ const farcasterExhausted = !newCursor;
+
+ return {
+ combinedMessages,
+ newCursor,
+ bothExhausted: dbExhausted && farcasterExhausted,
+ userIDs: farcasterResult?.userIDs ?? [],
+ };
+}
+
+type Props = {
+ +children: React.Node,
+};
+
+function FarcasterMessageFetchingProvider(props: Props): React.Node {
+ const { children } = props;
+
+ const [threadStates, setThreadStates] = React.useState<{
+ [threadID: string]: ThreadFetchingState,
+ }>({});
+
+ const getState = React.useCallback(
+ (threadID: string): ThreadFetchingState => {
+ return (
+ threadStates[threadID] ?? {
+ farcasterCursor: null,
+ dbExhausted: false,
+ farcasterExhausted: false,
+ }
+ );
+ },
+ [threadStates],
+ );
+
+ const updateState = React.useCallback(
+ (threadID: string, updates: Partial<ThreadFetchingState>) => {
+ setThreadStates(prevStates => ({
+ ...prevStates,
+ [threadID]: {
+ ...getState(threadID),
+ ...updates,
+ },
+ }));
+ },
+ [getState],
+ );
+
+ const fetchFarcasterMessagesForConversation =
+ useFetchMessagesForConversation();
+ const dispatch = useDispatch();
+ const fetchMoreMessages = React.useCallback(
+ async (
+ threadID: string,
+ numMessages: number,
+ currentOffset: number,
+ ): Promise<void> => {
+ const state =
+ currentOffset === 0
+ ? {
+ farcasterCursor: null,
+ dbExhausted: false,
+ farcasterExhausted: false,
+ }
+ : getState(threadID);
+ const conversationID = conversationIDFromFarcasterThreadID(threadID);
+
+ const dbPromise = !state.dbExhausted
+ ? fetchMessagesFromDB(threadID, numMessages, currentOffset)
+ : Promise.resolve(null);
+
+ const farcasterPromise = !state.farcasterExhausted
+ ? fetchFarcasterMessagesForConversation(
+ conversationID,
+ numMessages,
+ state.farcasterCursor ?? null,
+ )
+ : Promise.resolve(null);
+
+ const [dbResult, farcasterResult] = await Promise.allSettled([
+ dbPromise,
+ farcasterPromise,
+ ]);
+
+ const processedResults = processResults({
+ dbResult: dbResult.status === 'fulfilled' ? dbResult.value : null,
+ farcasterResult:
+ farcasterResult.status === 'fulfilled' ? farcasterResult.value : null,
+ });
+
+ updateState(threadID, {
+ farcasterCursor: processedResults.newCursor,
+ dbExhausted:
+ dbResult.status === 'fulfilled' &&
+ dbResult.value?.truncationStatus ===
+ messageTruncationStatus.EXHAUSTIVE,
+ farcasterExhausted: !processedResults.newCursor,
+ });
+
+ dispatch({
+ type: processFarcasterOpsActionType,
+ payload: {
+ rawMessageInfos: processedResults.combinedMessages,
+ updateInfos: ([]: Array<ClientUpdateInfo>),
+ userIDs: processedResults.userIDs,
+ truncationStatuses: {
+ [threadID]: processedResults.bothExhausted
+ ? messageTruncationStatus.EXHAUSTIVE
+ : messageTruncationStatus.UNCHANGED,
+ },
+ },
+ });
+ },
+ [getState, fetchFarcasterMessagesForConversation, updateState, dispatch],
+ );
+
+ const contextValue = React.useMemo(
+ () => ({
+ fetchMoreMessages,
+ }),
+ [fetchMoreMessages],
+ );
+
+ return (
+ <FarcasterMessageFetchingContext.Provider value={contextValue}>
+ {children}
+ </FarcasterMessageFetchingContext.Provider>
+ );
+}
+
+function useFarcasterMessageFetching(): FarcasterMessageFetchingContextType {
+ const context = React.useContext(FarcasterMessageFetchingContext);
+ invariant(context, 'FarcasterMessageFetchingContext must be set');
+ return context;
+}
+
+export { FarcasterMessageFetchingProvider, useFarcasterMessageFetching };
diff --git a/lib/shared/message-utils.js b/lib/shared/message-utils.js
--- a/lib/shared/message-utils.js
+++ b/lib/shared/message-utils.js
@@ -4,6 +4,7 @@
import _maxBy from 'lodash/fp/maxBy.js';
import * as React from 'react';
+import { useFarcasterMessageFetching } from './farcaster/farcaster-message-fetching-context.js';
import { codeBlockRegex, type ParserRules } from './markdown.js';
import type { CreationSideEffectsFunc } from './messages/message-spec.js';
import { messageSpecs } from './messages/message-specs.js';
@@ -637,6 +638,7 @@
const callFetchMessagesBeforeCursor = useFetchMessagesBeforeCursor();
const callFetchMostRecentMessages = useFetchMostRecentMessages();
const dispatchActionPromise = useDispatchActionPromise();
+ const farcasterMessageFetching = useFarcasterMessageFetching();
React.useEffect(() => {
registerFetchKey(fetchMessagesBeforeCursorActionTypes);
@@ -664,6 +666,7 @@
keyserverFetchMessagesBeforeCursor: callFetchMessagesBeforeCursor,
keyserverFetchMostRecentMessages: callFetchMostRecentMessages,
dispatchActionPromise,
+ farcasterMessageFetching,
},
);
},
@@ -671,6 +674,7 @@
callFetchMessagesBeforeCursor,
callFetchMostRecentMessages,
dispatchActionPromise,
+ farcasterMessageFetching,
messageIDs?.length,
oldestMessageServerID,
threadID,
@@ -705,5 +709,6 @@
isInvalidPinSourceForThread,
isUnableToBeRenderedIndependently,
findNewestMessageTimePerKeyserver,
+ fetchMessagesFromDB,
useFetchMessages,
};
diff --git a/lib/shared/threads/protocols/farcaster-thread-protocol.js b/lib/shared/threads/protocols/farcaster-thread-protocol.js
--- a/lib/shared/threads/protocols/farcaster-thread-protocol.js
+++ b/lib/shared/threads/protocols/farcaster-thread-protocol.js
@@ -2,7 +2,6 @@
import invariant from 'invariant';
-import { fetchMessagesBeforeCursorActionTypes } from '../../../actions/message-actions.js';
import {
changeThreadMemberRolesActionTypes,
changeThreadSettingsActionTypes,
@@ -672,18 +671,11 @@
) => {
const { threadID, numMessagesToFetch, currentNumberOfFetchedMessages } =
input;
- const promise = (async () => {
- return await utils.fetchMessagesFromDB(
- threadID,
- numMessagesToFetch ?? defaultNumberPerThread,
- currentNumberOfFetchedMessages,
- );
- })();
- void utils.dispatchActionPromise(
- fetchMessagesBeforeCursorActionTypes,
- promise,
+ await utils.farcasterMessageFetching.fetchMoreMessages(
+ threadID,
+ numMessagesToFetch ?? defaultNumberPerThread,
+ currentNumberOfFetchedMessages,
);
- await promise;
},
createPendingThread: (
@@ -820,8 +812,11 @@
input: ProtocolOnOpenThreadInput,
utils: OnOpenThreadUtils,
) => {
- const conversationID = conversationIDFromFarcasterThreadID(input.threadID);
- void utils.farcasterRefreshConversation(conversationID);
+ void utils.farcasterMessageFetching.fetchMoreMessages(
+ input.threadID,
+ defaultNumberPerThread,
+ 0,
+ );
},
threadIDMatchesProtocol: (threadID: string): boolean => {
diff --git a/lib/shared/threads/thread-spec.js b/lib/shared/threads/thread-spec.js
--- a/lib/shared/threads/thread-spec.js
+++ b/lib/shared/threads/thread-spec.js
@@ -101,6 +101,7 @@
SendReactionInput,
} from '../farcaster/farcaster-api.js';
import type { FarcasterConversation } from '../farcaster/farcaster-conversation-types.js';
+import type { FarcasterMessageFetchingContextType } from '../farcaster/farcaster-message-fetching-context.js';
import type { FetchMessagesFromDBType } from '../message-utils.js';
import type {
CreationSideEffectsFunc,
@@ -314,6 +315,7 @@
+keyserverFetchMessagesBeforeCursor: FetchMessagesBeforeCursorInput => Promise<FetchMessageInfosPayload>,
+keyserverFetchMostRecentMessages: FetchMostRecentMessagesInput => Promise<FetchMessageInfosPayload>,
+dispatchActionPromise: DispatchActionPromise,
+ +farcasterMessageFetching: FarcasterMessageFetchingContextType,
};
export type ProtocolCreatePendingThreadInput = {
@@ -367,6 +369,7 @@
};
export type OnOpenThreadUtils = {
+farcasterRefreshConversation: (conversationID: string) => Promise<mixed>,
+ +farcasterMessageFetching: FarcasterMessageFetchingContextType,
};
export type ProtocolName = 'Comm DM' | 'Farcaster DC' | 'Keyserver';
diff --git a/native/chat/message-list-container.react.js b/native/chat/message-list-container.react.js
--- a/native/chat/message-list-container.react.js
+++ b/native/chat/message-list-container.react.js
@@ -11,16 +11,14 @@
import genesis from 'lib/facts/genesis.js';
import { threadInfoSelector } from 'lib/selectors/thread-selectors.js';
import { userInfoSelectorForPotentialMembers } from 'lib/selectors/user-selectors.js';
-import { useRefreshFarcasterConversation } from 'lib/shared/farcaster/farcaster-hooks.js';
+import { useFarcasterThreadRefresher } from 'lib/shared/farcaster/farcaster-hooks.js';
+import { useIsAppForegrounded } from 'lib/shared/lifecycle-utils.js';
import {
usePotentialMemberItems,
useSearchUsers,
} from 'lib/shared/search-utils.js';
import { useExistingThreadInfoFinder } from 'lib/shared/thread-utils.js';
-import {
- threadTypeIsPersonal,
- threadSpecs,
-} from 'lib/shared/threads/thread-specs.js';
+import { threadTypeIsPersonal } from 'lib/shared/threads/thread-specs.js';
import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
import type { AccountUserInfo, UserListItem } from 'lib/types/user-types.js';
import { pinnedMessageCountText } from 'lib/utils/message-pinning-utils.js';
@@ -434,19 +432,12 @@
colors.panelBackgroundLabel,
]);
- const prevActiveThreadID = React.useRef(threadInfo.id);
- const farcasterRefreshConversation = useRefreshFarcasterConversation();
- React.useEffect(() => {
- if (prevActiveThreadID !== threadInfo.id) {
- threadSpecs[threadInfo.type]
- .protocol()
- .onOpenThread?.(
- { threadID: threadInfo.id },
- { farcasterRefreshConversation },
- );
- prevActiveThreadID.current = threadInfo.id;
- }
- }, [farcasterRefreshConversation, threadInfo.id, threadInfo.type]);
+ const isAppForegrounded = useIsAppForegrounded();
+ useFarcasterThreadRefresher(
+ threadInfo.id,
+ threadInfo.type,
+ isAppForegrounded,
+ );
let relationshipPrompt = null;
if (threadTypeIsPersonal(threadInfo.type)) {
diff --git a/native/root.react.js b/native/root.react.js
--- a/native/root.react.js
+++ b/native/root.react.js
@@ -52,6 +52,7 @@
import { IdentitySearchProvider } from 'lib/identity-search/identity-search-context.js';
import { CallKeyserverEndpointProvider } from 'lib/keyserver-conn/call-keyserver-endpoint-provider.react.js';
import KeyserverConnectionsHandler from 'lib/keyserver-conn/keyserver-connections-handler.js';
+import { FarcasterMessageFetchingProvider } from 'lib/shared/farcaster/farcaster-message-fetching-context.js';
import { TunnelbrokerProvider } from 'lib/tunnelbroker/tunnelbroker-context.js';
import { actionLogger } from 'lib/utils/action-logger.js';
import { useFullBackupSupportEnabled } from 'lib/utils/services-utils.js';
@@ -340,7 +341,9 @@
<GlobalSearchIndexProvider>
<NUXTipsContextProvider>
<BackupHandlerContextProvider>
- <RootNavigator />
+ <FarcasterMessageFetchingProvider>
+ <RootNavigator />
+ </FarcasterMessageFetchingProvider>
</BackupHandlerContextProvider>
</NUXTipsContextProvider>
</GlobalSearchIndexProvider>
diff --git a/web/app.react.js b/web/app.react.js
--- a/web/app.react.js
+++ b/web/app.react.js
@@ -43,6 +43,7 @@
combineLoadingStatuses,
} from 'lib/selectors/loading-selectors.js';
import { isLoggedIn } from 'lib/selectors/user-selectors.js';
+import { FarcasterMessageFetchingProvider } from 'lib/shared/farcaster/farcaster-message-fetching-context.js';
import { extractMajorDesktopVersion } from 'lib/shared/version-utils.js';
import type { SecondaryTunnelbrokerConnection } from 'lib/tunnelbroker/secondary-tunnelbroker-connection.js';
import { TunnelbrokerProvider } from 'lib/tunnelbroker/tunnelbroker-context.js';
@@ -235,8 +236,10 @@
<EditUserAvatarProvider>
<StaffContextProvider>
<MemberListSidebarProvider>
- {this.renderMainContent()}
- {this.props.modals}
+ <FarcasterMessageFetchingProvider>
+ {this.renderMainContent()}
+ {this.props.modals}
+ </FarcasterMessageFetchingProvider>
</MemberListSidebarProvider>
</StaffContextProvider>
</EditUserAvatarProvider>
diff --git a/web/chat/chat-message-list-container.react.js b/web/chat/chat-message-list-container.react.js
--- a/web/chat/chat-message-list-container.react.js
+++ b/web/chat/chat-message-list-container.react.js
@@ -7,9 +7,8 @@
import { NativeTypes } from 'react-dnd-html5-backend';
import { useProtocolSelection } from 'lib/contexts/protocol-selection-context.js';
-import { useRefreshFarcasterConversation } from 'lib/shared/farcaster/farcaster-hooks.js';
+import { useFarcasterThreadRefresher } from 'lib/shared/farcaster/farcaster-hooks.js';
import { threadIsPending } from 'lib/shared/thread-utils.js';
-import { threadSpecs } from 'lib/shared/threads/thread-specs.js';
import { useWatchThread } from 'lib/shared/watch-thread-utils.js';
import { useDispatch } from 'lib/utils/redux-utils.js';
@@ -20,6 +19,7 @@
import ThreadTopBar from './thread-top-bar.react.js';
import { InputStateContext } from '../input/input-state.js';
import { updateNavInfoActionType } from '../redux/action-types.js';
+import { useSelector } from '../redux/redux-utils.js';
import {
useThreadInfoForPossiblyPendingThread,
useInfosForPendingThread,
@@ -150,19 +150,12 @@
);
}, [inputState, isChatCreation, selectedUserInfos, threadInfo]);
- const prevActiveThreadID = React.useRef(activeChatThreadID);
- const farcasterRefreshConversation = useRefreshFarcasterConversation();
- React.useEffect(() => {
- if (prevActiveThreadID !== activeChatThreadID && activeChatThreadID) {
- threadSpecs[threadInfo.type]
- .protocol()
- .onOpenThread?.(
- { threadID: activeChatThreadID },
- { farcasterRefreshConversation },
- );
- prevActiveThreadID.current = activeChatThreadID;
- }
- }, [farcasterRefreshConversation, activeChatThreadID, threadInfo.type]);
+ const windowActive = useSelector(state => state.windowActive);
+ useFarcasterThreadRefresher(
+ activeChatThreadID,
+ threadInfo.type,
+ windowActive,
+ );
return connectDropTarget(
<div className={containerStyle} ref={containerRef}>
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Dec 6, 10:43 PM (23 h, 1 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5841221
Default Alt Text
D15375.1765060982.diff (22 KB)
Attached To
Mode
D15375: [lib] Load Farcaster messages while scrolling
Attached
Detach File
Event Timeline
Log In to Comment