diff --git a/native/navigation/route-names.js b/native/navigation/route-names.js --- a/native/navigation/route-names.js +++ b/native/navigation/route-names.js @@ -48,6 +48,7 @@ import type { VideoPlaybackModalParams } from '../media/video-playback-modal.react.js'; import type { CustomServerModalParams } from '../profile/custom-server-modal.react.js'; import type { KeyserverSelectionBottomSheetParams } from '../profile/keyserver-selection-bottom-sheet.react.js'; +import type { LinkedDevicesBottomSheetParams } from '../profile/linked-devices-bottom-sheet.react.js'; import type { UserRelationshipTooltipModalParams } from '../profile/user-relationship-tooltip-modal.react.js'; import type { ChangeRolesScreenParams } from '../roles/change-roles-screen.react.js'; import type { CommunityRolesScreenParams } from '../roles/community-roles-screen.react.js'; @@ -196,7 +197,7 @@ +UserProfileBottomSheetNavigator: void, +TunnelbrokerMenu: void, +KeyserverSelectionBottomSheet: KeyserverSelectionBottomSheetParams, - +LinkedDevicesBottomSheet: void, + +LinkedDevicesBottomSheet: LinkedDevicesBottomSheetParams, +ConnectFarcasterBottomSheet: void, +TagFarcasterChannelNavigator: void, +CreateMissingSIWEBackupMessage: void, diff --git a/native/profile/linked-devices-bottom-sheet.react.js b/native/profile/linked-devices-bottom-sheet.react.js --- a/native/profile/linked-devices-bottom-sheet.react.js +++ b/native/profile/linked-devices-bottom-sheet.react.js @@ -1,13 +1,33 @@ // @flow +import invariant from 'invariant'; import * as React from 'react'; -import { View } from 'react-native'; +import { View, Text } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useBroadcastDeviceListUpdates } from 'lib/hooks/peer-list-hooks.js'; +import { getAllPeerDevices } from 'lib/selectors/user-selectors.js'; +import { removeDeviceFromDeviceList } from 'lib/shared/device-list-utils.js'; +import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; +import { usePeerToPeerCommunication } from 'lib/tunnelbroker/peer-to-peer-context.js'; +import { + userActionsP2PMessageTypes, + type DeviceLogoutP2PMessage, +} from 'lib/types/tunnelbroker/user-actions-peer-to-peer-message-types.js'; +import { useSelector } from 'lib/utils/redux-utils.js'; + +import { BottomSheetContext } from '../bottom-sheet/bottom-sheet-provider.react.js'; import BottomSheet from '../bottom-sheet/bottom-sheet.react.js'; +import Button from '../components/button.react.js'; import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useStyles } from '../themes/colors.js'; import type { BottomSheetRef } from '../types/bottom-sheet.js'; +import Alert from '../utils/alert.js'; + +export type LinkedDevicesBottomSheetParams = { + +deviceID: string, +}; type Props = { +navigation: RootNavigationProp<'LinkedDevicesBottomSheet'>, @@ -15,17 +35,119 @@ }; function LinkedDevicesBottomSheet(props: Props): React.Node { - const { navigation } = props; + const { + navigation, + route: { + params: { deviceID }, + }, + } = props; const { goBack } = navigation; + const identityContext = React.useContext(IdentityClientContext); + invariant(identityContext, 'identity context not set'); + const { identityClient, getAuthMetadata } = identityContext; + + const broadcastDeviceListUpdates = useBroadcastDeviceListUpdates(); + const { broadcastEphemeralMessage } = usePeerToPeerCommunication(); + const allPeerDevices = useSelector(getAllPeerDevices); + + const bottomSheetContext = React.useContext(BottomSheetContext); + invariant(bottomSheetContext, 'bottomSheetContext should be set'); + const { setContentHeight } = bottomSheetContext; + const bottomSheetRef = React.useRef(); + const removeDeviceContainerRef = + React.useRef>(); const styles = useStyles(unboundStyles); + const insets = useSafeAreaInsets(); + + const handleDeviceRemoval = React.useCallback(async () => { + const authMetadata = await getAuthMetadata(); + const { userID } = authMetadata; + if (!userID) { + throw new Error('No user ID'); + } + + try { + await removeDeviceFromDeviceList(identityClient, userID, deviceID); + } catch (err) { + console.log('Primary device error:', err); + Alert.alert( + 'Removing device failed', + 'Failed to update the device list', + [{ text: 'OK' }], + ); + bottomSheetRef.current?.close(); + } + + const messageContents: DeviceLogoutP2PMessage = { + type: userActionsP2PMessageTypes.LOG_OUT_DEVICE, + }; + + const sendLogoutMessagePromise = broadcastEphemeralMessage( + JSON.stringify(messageContents), + [{ userID, deviceID }], + authMetadata, + ); + const broadcastUpdatePromise = broadcastDeviceListUpdates( + allPeerDevices.filter(peerDeviceID => deviceID !== peerDeviceID), + ); + await Promise.all([sendLogoutMessagePromise, broadcastUpdatePromise]); + bottomSheetRef.current?.close(); + }, [ + broadcastDeviceListUpdates, + broadcastEphemeralMessage, + deviceID, + allPeerDevices, + getAuthMetadata, + identityClient, + ]); + + const confirmDeviceRemoval = () => { + Alert.alert( + 'Remove device', + 'Are you sure you want to remove this device?', + [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Remove', style: 'destructive', onPress: handleDeviceRemoval }, + ], + { cancelable: true }, + ); + }; + + const onLayout = React.useCallback(() => { + removeDeviceContainerRef.current?.measure( + (x, y, width, height, pageX, pageY) => { + if ( + height === null || + height === undefined || + pageY === null || + pageY === undefined + ) { + return; + } + + setContentHeight(height + insets.bottom); + }, + ); + }, [insets.bottom, setContentHeight]); return ( - + + + ); } @@ -34,6 +156,15 @@ container: { paddingHorizontal: 16, }, + removeButtonContainer: { + backgroundColor: 'vibrantRedButton', + paddingVertical: 12, + borderRadius: 8, + alignItems: 'center', + }, + removeButtonText: { + color: 'floatingButtonLabel', + }, }; export default LinkedDevicesBottomSheet; diff --git a/native/profile/linked-devices-list-item.react.js b/native/profile/linked-devices-list-item.react.js --- a/native/profile/linked-devices-list-item.react.js +++ b/native/profile/linked-devices-list-item.react.js @@ -32,8 +32,11 @@ const onPress = React.useCallback(() => { navigate<'LinkedDevicesBottomSheet'>({ name: LinkedDevicesBottomSheetRouteName, + params: { + deviceID, + }, }); - }, [navigate]); + }, [deviceID, navigate]); const deviceType = platformDetails?.deviceType;