-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat: static & live location sharing (draft) #6694
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
5098f26
bd531f2
c91c399
7c01380
06de63b
f985d4d
5057700
1b35e25
72dc234
5450781
6f44e91
a991844
db4f0f3
3ef9f97
f59c26d
fdd3834
2152119
c4e2514
f4b2aec
bd47d05
366cd18
23ae666
7cc84af
43c0adc
8625c64
8712efc
90e3452
1311bd5
320705f
ad3c8dc
03af849
f17aa5b
3d83f5e
3eff38a
95ec9cd
ba6ca43
d695613
289bcfe
8bc29d2
6ea1090
1de7572
c37b1fa
a10295e
4e7a86f
c064b71
d458659
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package chat.rocket.reactnative | ||
|
|
||
| import android.app.Service | ||
| import android.content.Intent | ||
| import android.os.IBinder | ||
|
|
||
| /** | ||
| * Minimal stub for a location foreground service. | ||
| * We don't start it in the current app flow; this exists to satisfy | ||
| * Android's requirement that FOREGROUND_SERVICE_LOCATION has a matching | ||
| * service declaration. If started accidentally, stop immediately. | ||
| */ | ||
| class LocationService : Service() { | ||
| override fun onBind(intent: Intent?): IBinder? = null | ||
|
|
||
| override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { | ||
| // Not used in current implementation. Ensure we stop if invoked. | ||
| stopSelf() | ||
| return START_NOT_STICKY | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| { | ||
| "name": "RocketChatRN" | ||
| "name": "RocketChatRN", | ||
| "plugins": ["expo-web-browser"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,6 @@ | ||
| import React, { useContext } from 'react'; | ||
| import { Platform, PermissionsAndroid, InteractionManager, Alert } from 'react-native'; | ||
| import * as Location from 'expo-location'; | ||
|
|
||
| import { getSubscriptionByRoomId } from '../../../../lib/database/services/Subscription'; | ||
| import { BaseButton } from './BaseButton'; | ||
|
|
@@ -10,6 +12,12 @@ import { useAppSelector } from '../../../../lib/hooks/useAppSelector'; | |
| import { usePermissions } from '../../../../lib/hooks/usePermissions'; | ||
| import { useCanUploadFile, useChooseMedia } from '../../hooks'; | ||
| import { useRoomContext } from '../../../../views/RoomView/context'; | ||
| import { showErrorAlert } from '../../../../lib/methods/helpers'; | ||
| import { getCurrentPositionOnce } from '../../../../views/LocationShare/services/staticLocation'; | ||
| import type { MapProviderName } from '../../../../views/LocationShare/services/mapProviders'; | ||
| import { isLiveLocationActive, reopenLiveLocationModal } from '../../../../views/LocationShare/LiveLocationPreviewModal'; | ||
| import { useUserPreferences } from '../../../../lib/methods/userPreferences'; | ||
| import { MAP_PROVIDER_PREFERENCE_KEY, MAP_PROVIDER_DEFAULT } from '../../../../lib/constants/keys'; | ||
|
|
||
| export const ActionsButton = () => { | ||
| 'use memo'; | ||
|
|
@@ -25,6 +33,26 @@ export const ActionsButton = () => { | |
| }); | ||
| const { showActionSheet, hideActionSheet } = useActionSheet(); | ||
| const isMasterDetail = useAppSelector(state => state.app.isMasterDetail); | ||
| const userId = useAppSelector(state => state.login.user.id); | ||
|
|
||
| const [mapProvider] = useUserPreferences<MapProviderName>(`${MAP_PROVIDER_PREFERENCE_KEY}_${userId}`, MAP_PROVIDER_DEFAULT); | ||
|
|
||
| const sheetBusyRef = React.useRef(false); | ||
| const openSheetSafely = (fn: () => void, delayMs = 350) => { | ||
| if (sheetBusyRef.current) return; | ||
| sheetBusyRef.current = true; | ||
|
|
||
| hideActionSheet(); | ||
| InteractionManager.runAfterInteractions(() => { | ||
| setTimeout(() => { | ||
| try { | ||
| fn(); | ||
| } finally { | ||
| sheetBusyRef.current = false; | ||
| } | ||
| }, delayMs); | ||
| }); | ||
| }; | ||
|
|
||
| const createDiscussion = async () => { | ||
| if (!rid) return; | ||
|
|
@@ -37,62 +65,198 @@ export const ActionsButton = () => { | |
| } | ||
| }; | ||
|
|
||
| const openCurrentPreview = async (provider: MapProviderName) => { | ||
| try { | ||
| if (!rid) { | ||
| showErrorAlert(I18n.t('Room_not_available'), I18n.t('Oops')); | ||
| return; | ||
| } | ||
|
|
||
| if (Platform.OS === 'android') { | ||
| const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION); | ||
| if (granted !== PermissionsAndroid.RESULTS.GRANTED) { | ||
| showErrorAlert(I18n.t('Location_permission_required'), I18n.t('Oops')); | ||
| return; | ||
| } | ||
| } else { | ||
| const { status } = await Location.requestForegroundPermissionsAsync(); | ||
| if (status !== 'granted') { | ||
| showErrorAlert(I18n.t('Location_permission_required'), I18n.t('Oops')); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| const coords = await getCurrentPositionOnce(); | ||
|
|
||
| const params = { | ||
| rid, | ||
| tmid, | ||
| provider, | ||
| coords | ||
| }; | ||
|
|
||
| InteractionManager.runAfterInteractions(() => { | ||
| if (isMasterDetail) { | ||
| Navigation.navigate('ModalStackNavigator', { screen: 'LocationPreviewModal', params }); | ||
| } else { | ||
| Navigation.navigate('LocationPreviewModal', params); | ||
| } | ||
| }); | ||
| } catch (e) { | ||
| const error = e as Error; | ||
| showErrorAlert(error?.message || I18n.t('Could_not_get_location'), I18n.t('Oops')); | ||
| } | ||
| }; | ||
|
|
||
| const openLivePreview = async (provider: MapProviderName) => { | ||
| try { | ||
| if (isLiveLocationActive()) { | ||
| Alert.alert(I18n.t('Live_Location_Active'), I18n.t('Live_Location_Active_Block_Message'), [ | ||
| { text: I18n.t('View_Current_Session'), onPress: () => reopenLiveLocationModal() }, | ||
| { text: I18n.t('Cancel'), style: 'cancel' } | ||
| ]); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move return to here, after calling Alert.alert |
||
| return; | ||
| } | ||
| if (!rid) { | ||
| showErrorAlert(I18n.t('Room_not_available'), I18n.t('Oops')); | ||
| return; | ||
| } | ||
|
|
||
| if (Platform.OS === 'android') { | ||
| const res = await PermissionsAndroid.requestMultiple([ | ||
| PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, | ||
| PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION | ||
| ]); | ||
| const fine = res[PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION]; | ||
| const coarse = res[PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION]; | ||
| if (fine !== PermissionsAndroid.RESULTS.GRANTED && coarse !== PermissionsAndroid.RESULTS.GRANTED) { | ||
| throw new Error(I18n.t('Permission_denied')); | ||
| } | ||
| } else { | ||
| const { status } = await Location.requestForegroundPermissionsAsync(); | ||
| if (status !== 'granted') { | ||
| throw new Error(I18n.t('Location_permission_required')); | ||
| } | ||
| } | ||
|
|
||
| const params = { | ||
| rid, | ||
| tmid, | ||
| provider | ||
| }; | ||
|
|
||
| InteractionManager.runAfterInteractions(() => { | ||
| // @ts-ignore | ||
| if (isMasterDetail) { | ||
| Navigation.navigate('ModalStackNavigator', { screen: 'LiveLocationPreviewModal', params }); | ||
| } else { | ||
| Navigation.navigate('LiveLocationPreviewModal', params); | ||
| } | ||
| }); | ||
| } catch (e) { | ||
| const error = e as Error; | ||
| showErrorAlert(error?.message || I18n.t('Could_not_get_location'), I18n.t('Oops')); | ||
| } | ||
| }; | ||
|
Comment on lines
111
to
160
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Remove @ts-ignore by updating navigation types. The live location preview flow is well-structured, but the Ensure that the navigation types (e.g., in LiveLocationPreviewModal: {
rid: string;
tmid?: string;
provider: MapProviderName;
googleKey?: string;
osmKey?: string;
liveLocationId?: string;
ownerName?: string;
isTracking?: boolean;
};This will eliminate the need for
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolve this too
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| const openModeSheetForProvider = (provider: MapProviderName) => { | ||
| const modeOptions: TActionSheetOptionsItem[] = [ | ||
| { | ||
| title: I18n.t('Share_current_location'), | ||
| icon: 'pin-map', | ||
| onPress: () => { | ||
| openSheetSafely(() => openCurrentPreview(provider)); | ||
| } | ||
| }, | ||
| { | ||
| title: I18n.t('Start_live_location'), | ||
| icon: 'live', | ||
| onPress: () => { | ||
| openSheetSafely(() => openLivePreview(provider)); | ||
| } | ||
| } | ||
| ]; | ||
| showActionSheet({ options: modeOptions }); | ||
| }; | ||
|
|
||
| const onPress = () => { | ||
| const options: TActionSheetOptionsItem[] = []; | ||
|
|
||
| if (t === 'l' && permissionToViewCannedResponses) { | ||
| options.push({ | ||
| title: I18n.t('Canned_Responses'), | ||
| icon: 'canned-response', | ||
| onPress: () => Navigation.navigate('CannedResponsesListView', { rid }) | ||
| onPress: () => { | ||
| hideActionSheet(); | ||
| InteractionManager.runAfterInteractions(() => { | ||
| Navigation.navigate('CannedResponsesListView', { rid }); | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| if (permissionToUpload) { | ||
| options.push( | ||
| { | ||
| title: I18n.t('Take_a_photo'), | ||
| icon: 'camera-photo', | ||
| onPress: () => { | ||
| hideActionSheet(); | ||
| // This is necessary because the action sheet does not close properly on Android | ||
| setTimeout(() => { | ||
| InteractionManager.runAfterInteractions(() => { | ||
| takePhoto(); | ||
| }, 250); | ||
| }); | ||
| } | ||
| }, | ||
| { | ||
| title: I18n.t('Take_a_video'), | ||
| icon: 'camera', | ||
| onPress: () => { | ||
| hideActionSheet(); | ||
| // This is necessary because the action sheet does not close properly on Android | ||
| setTimeout(() => { | ||
| InteractionManager.runAfterInteractions(() => { | ||
| takeVideo(); | ||
| }, 250); | ||
| }); | ||
| } | ||
| }, | ||
| { | ||
| title: I18n.t('Choose_from_library'), | ||
| icon: 'image', | ||
| onPress: () => { | ||
| hideActionSheet(); | ||
| // This is necessary because the action sheet does not close properly on Android | ||
| setTimeout(() => { | ||
| InteractionManager.runAfterInteractions(() => { | ||
| chooseFromLibrary(); | ||
| }, 250); | ||
| }); | ||
| } | ||
| }, | ||
| { | ||
| title: I18n.t('Choose_file'), | ||
| icon: 'attach', | ||
| onPress: () => chooseFile() | ||
| onPress: () => { | ||
| hideActionSheet(); | ||
| InteractionManager.runAfterInteractions(() => { | ||
| chooseFile(); | ||
| }); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
|
|
||
| options.push({ | ||
| title: I18n.t('Create_Discussion'), | ||
| icon: 'discussions', | ||
| onPress: () => createDiscussion() | ||
| onPress: () => { | ||
| hideActionSheet(); | ||
| InteractionManager.runAfterInteractions(() => { | ||
| createDiscussion(); | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| options.push({ | ||
| title: I18n.t('Share_Location'), | ||
| icon: 'pin-map', | ||
| onPress: () => { | ||
| openSheetSafely(() => openModeSheetForProvider(mapProvider)); | ||
| } | ||
| }); | ||
|
|
||
| closeEmojiKeyboardAndAction(showActionSheet, { options }); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.