-
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 17 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
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 |
|---|---|---|
|
|
@@ -19,6 +19,16 @@ | |
| <!-- android 13 media permission --> | ||
| <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> | ||
|
|
||
| <!-- Location --> | ||
| <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> | ||
| <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> | ||
|
|
||
| <!-- If you stream/live share or track in background --> | ||
| <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | ||
| <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" /> | ||
| <!-- Optional: only if you truly need background updates on Android 10+ --> | ||
| <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> | ||
|
Comment on lines
+29
to
+30
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. 🧩 Analysis chainBackground location permission requires strong justification. The Verify that:
Consider whether this permission can be removed if the feature only needs location while the app is in the foreground or in a foreground service state. 🏁 Script executed: #!/bin/bash
# Description: Check if background location subscription is actually used in the codebase
echo "Searching for background location configuration or task registration..."
rg -n --type typescript --type javascript -C3 'Background|TaskManager|startLocationUpdates.*background' app/Length of output: 106 🏁 Script executed: #!/bin/bash
echo "Searching for background location usage in codebase..."
rg -n --ignore-case -C3 'backgroundLocation' -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' .
rg -n --ignore-case -C3 'startLocationUpdates' .
rg -n --ignore-case -C3 'registerTask' .
rg -n --ignore-case -C3 'TaskManager' .Length of output: 54 🏁 Script executed: #!/bin/bash
sed -n '1,200p' android/app/src/main/AndroidManifest.xmlLength of output: 5160 Remove unused ACCESS_BACKGROUND_LOCATION permission 🤖 Prompt for AI Agents |
||
|
|
||
| <application | ||
| android:name="chat.rocket.reactnative.MainApplication" | ||
| android:allowBackup="false" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,10 @@ | ||
| { | ||
| "name": "RocketChatRN" | ||
| "name": "RocketChatRN", | ||
| "plugins": ["expo-web-browser"], | ||
| "expo": { | ||
| "extra": { | ||
| "GOOGLE_MAPS_API_KEY": "", | ||
| "OSM_API_KEY": "" | ||
| } | ||
| } | ||
|
Comment on lines
+4
to
+9
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. 🧩 Analysis chainEnsure runtime handling of empty API keys. The 🏁 Script executed: #!/bin/bash
# Description: Check how the app handles empty API keys in map provider logic
rg -nP --type=ts --type=tsx -C3 'GOOGLE_MAPS_API_KEY|OSM_API_KEY' -g '!node_modules' -g '!*.json'Length of output: 27 🏁 Script executed: #!/bin/bash
# Search for API key references across code
rg -n 'GOOGLE_MAPS_API_KEY|OSM_API_KEY' --glob '**/*.{ts,tsx,js,jsx}' --glob '!node_modules/**'Length of output: 1264 🏁 Script executed: #!/bin/bash
rg -n "import .* from 'expo-constants'" -C3 --glob "**/*.{ts,tsx,js,jsx}" --glob "!node_modules/**"Length of output: 0 Implement runtime loading and graceful fallback for map API keys |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,7 +9,19 @@ | |
| import { useAppSelector, usePermissions } from '../../../../lib/hooks'; | ||
| import { useCanUploadFile, useChooseMedia } from '../../hooks'; | ||
| import { useRoomContext } from '../../../../views/RoomView/context'; | ||
| import { Platform, PermissionsAndroid, InteractionManager } from 'react-native'; | ||
|
Check failure on line 12 in app/containers/MessageComposer/components/Buttons/ActionsButton.tsx
|
||
| import * as Location from 'expo-location'; | ||
|
Check failure on line 13 in app/containers/MessageComposer/components/Buttons/ActionsButton.tsx
|
||
| import { showErrorAlert } from '../../../../lib/methods/helpers'; | ||
|
Check failure on line 14 in app/containers/MessageComposer/components/Buttons/ActionsButton.tsx
|
||
| import { getCurrentPositionOnce } from '../../../../views/LocationShare/services/staticLocation'; | ||
| import { MapProviderName } from '../../../../views/LocationShare/services/mapProviders'; | ||
| import { useUserPreferences } from '../../../../lib/methods'; | ||
| import { | ||
|
Check failure on line 18 in app/containers/MessageComposer/components/Buttons/ActionsButton.tsx
|
||
| MAP_PROVIDER_PREFERENCE_KEY, | ||
| GOOGLE_MAPS_API_KEY_PREFERENCE_KEY, | ||
| OSM_API_KEY_PREFERENCE_KEY, | ||
| MAP_PROVIDER_DEFAULT | ||
| } from '../../../../lib/constants'; | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
|
Check failure on line 24 in app/containers/MessageComposer/components/Buttons/ActionsButton.tsx
|
||
| export const ActionsButton = () => { | ||
| const { rid, tmid, t } = useRoomContext(); | ||
| const { closeEmojiKeyboardAndAction } = useContext(MessageInnerContext); | ||
|
|
@@ -22,6 +34,30 @@ | |
| }); | ||
| 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 [googleApiKey] = useUserPreferences<string>(`${GOOGLE_MAPS_API_KEY_PREFERENCE_KEY}_${userId}`, ''); | ||
| const [osmApiKey] = useUserPreferences<string>(`${OSM_API_KEY_PREFERENCE_KEY}_${userId}`, ''); | ||
|
|
||
| // --- Sheet transition helpers --- | ||
| const sheetBusyRef = React.useRef(false); | ||
| /** Safely close the current ActionSheet and then run `fn` (open next sheet) */ | ||
| 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; | ||
|
|
@@ -34,62 +70,207 @@ | |
| } | ||
| }; | ||
|
|
||
| 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, | ||
| googleKey: provider === 'google' ? googleApiKey : undefined, | ||
| osmKey: provider === 'osm' ? osmApiKey : undefined | ||
| }; | ||
|
|
||
| InteractionManager.runAfterInteractions(() => { | ||
| if (isMasterDetail) { | ||
| Navigation.navigate('ModalStackNavigator', { screen: 'LocationPreviewModal', params }); | ||
| } else { | ||
| Navigation.navigate('LocationPreviewModal', params); | ||
| } | ||
| }); | ||
| } catch (e: any) { | ||
| showErrorAlert(e?.message || I18n.t('Could_not_get_location'), I18n.t('Oops')); | ||
| } | ||
| }; | ||
|
|
||
| const openLivePreview = async (provider: MapProviderName) => { | ||
| try { | ||
| 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, | ||
| googleKey: provider === 'google' ? googleApiKey : undefined, | ||
| osmKey: provider === 'osm' ? osmApiKey : undefined | ||
| }; | ||
|
|
||
| // Defer navigation until after sheets/animations are done | ||
| InteractionManager.runAfterInteractions(() => { | ||
| // @ts-ignore | ||
| if (isMasterDetail) { | ||
| Navigation.navigate('ModalStackNavigator', { screen: 'LiveLocationPreviewModal', params }); | ||
| } else { | ||
| Navigation.navigate('LiveLocationPreviewModal', params); | ||
| } | ||
| }); | ||
| } catch (e: any) { | ||
| showErrorAlert(e?.message || I18n.t('Could_not_get_location'), I18n.t('Oops')); | ||
| } | ||
| }; | ||
|
Comment on lines
+120
to
+164
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 |
||
|
|
||
| const openModeSheetForProvider = (provider: MapProviderName) => { | ||
| const modeOptions: TActionSheetOptionsItem[] = [ | ||
| { | ||
| title: I18n.t('Share_current_location'), | ||
| icon: 'pin-map', | ||
| onPress: () => { | ||
| // sheet -> navigation: close current sheet safely, then start flow | ||
| openSheetSafely(() => openCurrentPreview(provider)); | ||
| } | ||
| }, | ||
| { | ||
| title: I18n.t('Start_live_location'), | ||
| icon: 'live', | ||
| onPress: () => { | ||
| // sheet -> navigation: close current sheet safely, then start flow | ||
| 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: () => { | ||
| // Check if the user has configured API keys for their preferred provider | ||
| const needsApiKey = (mapProvider === 'google' && !googleApiKey) || (mapProvider === 'osm' && !osmApiKey); | ||
|
|
||
| if (needsApiKey) { | ||
| showErrorAlert( | ||
| I18n.t('API_key_required', { provider: mapProvider === 'google' ? 'Google Maps' : 'OpenStreetMap' }), | ||
| I18n.t('Please_configure_API_key_in_settings') | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| openSheetSafely(() => openModeSheetForProvider(mapProvider)); | ||
| } | ||
| }); | ||
|
|
||
| closeEmojiKeyboardAndAction(showActionSheet, { options }); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FOREGROUND_SERVICE_LOCATION requires service declaration in manifest.
The
FOREGROUND_SERVICE_LOCATIONpermission requires a corresponding<service>declaration withandroid:foregroundServiceType="location"in the manifest. Without this declaration, the permission is ineffective and location access from a foreground service will fail.Add a service declaration if live location tracking uses a foreground service:
If the app uses Expo or a library that handles this automatically, verify the service is properly configured.
🤖 Prompt for AI Agents