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 @@ -518,15 +518,20 @@ function useRefreshFarcasterConversation(): ( conversationID: string, + messagesLimit?: number, ) => Promise { const fetchConversationWithMessages = useFetchConversationWithMessages(); const dispatch = useDispatch(); return React.useCallback( - async (conversationID: string) => { + async (conversationID: string, messagesLimit?: number) => { const batchedUpdates = new BatchedUpdates(); - await fetchConversationWithMessages(conversationID, 20, batchedUpdates); + await fetchConversationWithMessages( + conversationID, + messagesLimit ?? 20, + batchedUpdates, + ); if (!batchedUpdates.isEmpty()) { dispatch({ diff --git a/lib/shared/threads/protocols/dm-thread-protocol.js b/lib/shared/threads/protocols/dm-thread-protocol.js --- a/lib/shared/threads/protocols/dm-thread-protocol.js +++ b/lib/shared/threads/protocols/dm-thread-protocol.js @@ -947,6 +947,7 @@ viewerCanUpdateOwnRole: false, protocolName: protocolNames.COMM_DM, canReactToRobotext: true, + supportsThreadRefreshing: false, }); function pendingThreadType(numberOfOtherMembers: number) { 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 @@ -964,6 +964,7 @@ viewerCanUpdateOwnRole: false, protocolName: protocolNames.FARCASTER_DC, canReactToRobotext: false, + supportsThreadRefreshing: true, }; function pendingThreadType(numberOfOtherMembers: number) { diff --git a/lib/shared/threads/protocols/keyserver-thread-protocol.js b/lib/shared/threads/protocols/keyserver-thread-protocol.js --- a/lib/shared/threads/protocols/keyserver-thread-protocol.js +++ b/lib/shared/threads/protocols/keyserver-thread-protocol.js @@ -751,6 +751,7 @@ viewerCanUpdateOwnRole: true, protocolName: protocolNames.KEYSERVER, canReactToRobotext: true, + supportsThreadRefreshing: false, }); function pendingThreadType(numberOfOtherMembers: number) { 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 @@ -537,6 +537,7 @@ +viewerCanUpdateOwnRole: boolean, +protocolName: ProtocolName, +canReactToRobotext: boolean, + +supportsThreadRefreshing: boolean, }; export type ThreadSpec< diff --git a/native/chat/settings/thread-settings-refresh.react.js b/native/chat/settings/thread-settings-refresh.react.js new file mode 100644 --- /dev/null +++ b/native/chat/settings/thread-settings-refresh.react.js @@ -0,0 +1,85 @@ +// @flow + +import Icon from '@expo/vector-icons/Ionicons.js'; +import * as React from 'react'; +import { View, Text, Platform } from 'react-native'; + +import Button from '../../components/button.react.js'; +import { useStyles } from '../../themes/colors.js'; + +const unboundStyles = { + container: { + flex: 1, + flexDirection: 'row', + paddingHorizontal: 12, + paddingVertical: 8, + justifyContent: 'center', + }, + icon: { + lineHeight: 20, + }, + refreshButton: { + paddingTop: Platform.OS === 'ios' ? 4 : 1, + }, + refreshIcon: { + color: 'panelForegroundSecondaryLabel', + }, + refreshRow: { + backgroundColor: 'panelForeground', + paddingHorizontal: 12, + }, + refreshText: { + color: 'panelForegroundSecondaryLabel', + flex: 1, + fontSize: 16, + }, + disabled: { + color: 'disabledButtonText', + }, +}; + +type RefreshProps = { + +onPress: () => Promise, +}; + +function ThreadSettingsRefresh(props: RefreshProps): React.Node { + const styles = useStyles(unboundStyles); + const [isRefreshing, setIsRefreshing] = React.useState(false); + + const onPressWrapper = React.useCallback(async () => { + if (isRefreshing) { + return; + } + setIsRefreshing(true); + try { + await props.onPress(); + } finally { + setIsRefreshing(false); + } + }, [isRefreshing, props]); + + const disabledStyle = isRefreshing ? styles.disabled : null; + + return ( + + + + ); +} + +export default ThreadSettingsRefresh; diff --git a/native/chat/settings/thread-settings.react.js b/native/chat/settings/thread-settings.react.js --- a/native/chat/settings/thread-settings.react.js +++ b/native/chat/settings/thread-settings.react.js @@ -31,6 +31,8 @@ childThreadInfos, threadInfoSelector, } from 'lib/selectors/thread-selectors.js'; +import { useRefreshFarcasterConversation } from 'lib/shared/farcaster/farcaster-hooks.js'; +import { conversationIDFromFarcasterThreadID } from 'lib/shared/id-utils.js'; import { getAvailableRelationshipButtons } from 'lib/shared/relationship-utils.js'; import { getSingleOtherUser, @@ -84,6 +86,7 @@ import ThreadSettingsParent from './thread-settings-parent.react.js'; import ThreadSettingsPromoteSidebar from './thread-settings-promote-sidebar.react.js'; import ThreadSettingsPushNotifs from './thread-settings-push-notifs.react.js'; +import ThreadSettingsRefresh from './thread-settings-refresh.react.js'; import ThreadSettingsVisibility from './thread-settings-visibility.react.js'; import ThreadAncestors from '../../components/thread-ancestors.react.js'; import { @@ -234,7 +237,7 @@ +verticalBounds: ?VerticalBounds, } | { - +itemType: 'promoteSidebar' | 'leaveThread' | 'deleteThread', + +itemType: 'promoteSidebar' | 'leaveThread' | 'deleteThread' | 'refresh', +key: string, +threadInfo: ResolvedThreadInfo, +navigate: ThreadSettingsNavigate, @@ -296,6 +299,7 @@ +canDeleteThread: boolean, +canManageInviteLinks: boolean, +inviteLinkExists: boolean, + +refreshFarcasterConversation: (threadID: string) => Promise, }; type State = { +numMembersShowing: number, @@ -758,6 +762,17 @@ ) => { const buttons = []; + const supportsThreadRefreshing = + threadSpecs[threadInfo.type].protocol().supportsThreadRefreshing; + if (supportsThreadRefreshing) { + buttons.push({ + itemType: 'refresh', + key: 'refresh', + threadInfo, + navigate, + }); + } + if (this.props.canPromoteSidebar) { buttons.push({ itemType: 'promoteSidebar', @@ -1055,6 +1070,8 @@ buttonStyle={item.buttonStyle} /> ); + } else if (item.itemType === 'refresh') { + return ; } else if (item.itemType === 'deleteThread') { return ( { + const { refreshFarcasterConversation, threadInfo } = this.props; + await refreshFarcasterConversation(threadInfo.id); + }; } const threadMembersChangeIsSaving = ( @@ -1347,6 +1369,19 @@ ); }, [callFetchPrimaryLinks, dispatchActionPromise, isCommunityRoot]); + const refreshFarcasterConversationHook = useRefreshFarcasterConversation(); + const refreshFarcasterConversation = React.useCallback( + async (farcasterThreadID: string) => { + const conversationID = + conversationIDFromFarcasterThreadID(farcasterThreadID); + await refreshFarcasterConversationHook( + conversationID, + Number.POSITIVE_INFINITY, + ); + }, + [refreshFarcasterConversationHook], + ); + return ( ); }, diff --git a/web/chat/thread-menu.react.js b/web/chat/thread-menu.react.js --- a/web/chat/thread-menu.react.js +++ b/web/chat/thread-menu.react.js @@ -10,6 +10,8 @@ childThreadInfos, otherUsersButNoOtherAdmins, } from 'lib/selectors/thread-selectors.js'; +import { useRefreshFarcasterConversation } from 'lib/shared/farcaster/farcaster-hooks.js'; +import { conversationIDFromFarcasterThreadID } from 'lib/shared/id-utils.js'; import { threadIsChannel, useThreadHasPermission, @@ -47,6 +49,43 @@ const { threadInfo } = props; const { onPromoteSidebar, canPromoteSidebar } = usePromoteSidebar(threadInfo); + const [isRefreshing, setIsRefreshing] = React.useState(false); + + const supportsThreadRefreshing = + threadSpecs[threadInfo.type].protocol().supportsThreadRefreshing; + + const refreshFarcasterConversationHook = useRefreshFarcasterConversation(); + const onClickRefresh = React.useCallback(async () => { + if (isRefreshing) { + return; + } + setIsRefreshing(true); + try { + const conversationID = conversationIDFromFarcasterThreadID(threadInfo.id); + await refreshFarcasterConversationHook( + conversationID, + Number.POSITIVE_INFINITY, + ); + } finally { + setIsRefreshing(false); + } + }, [isRefreshing, threadInfo.id, refreshFarcasterConversationHook]); + + const refreshItem = React.useMemo(() => { + if (!supportsThreadRefreshing) { + return null; + } + return ( + + ); + }, [supportsThreadRefreshing, isRefreshing, onClickRefresh]); + const onClickSettings = React.useCallback( () => pushModal(), [pushModal, threadInfo.id], @@ -303,6 +342,7 @@ notificationsItem, membersItem, threadMediaGalleryItem, + refreshItem, sidebarItem, viewSubchannelsItem, createSubchannelsItem, @@ -316,6 +356,7 @@ notificationsItem, membersItem, threadMediaGalleryItem, + refreshItem, sidebarItem, viewSubchannelsItem, promoteSidebar, diff --git a/web/components/menu-item.react.js b/web/components/menu-item.react.js --- a/web/components/menu-item.react.js +++ b/web/components/menu-item.react.js @@ -14,6 +14,7 @@ +onClick?: () => mixed, +text: string, +dangerous?: boolean, + +disabled?: boolean, }; export type MenuItemProps = | { @@ -26,7 +27,7 @@ }; function MenuItem(props: MenuItemProps): React.Node { - const { onClick, icon, iconComponent, text, dangerous } = props; + const { onClick, icon, iconComponent, text, dangerous, disabled } = props; const itemClasses = classNames(css.menuAction, { [css.menuActionDangerous]: dangerous, @@ -38,7 +39,7 @@ } return ( -