Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
5098f26
chore: remove obsolete webview patch
Aug 25, 2025
bd531f2
feat(mobile): implement static location sharing
Sep 1, 2025
c91c399
feat(mobile): implement static location sharing
Sep 8, 2025
7c01380
feat(mobile): implement live location sharing
Sep 22, 2025
06de63b
feat(mobile): implement live location sharing
Sep 22, 2025
f985d4d
feat(mobile): implement live location sharing
Sep 23, 2025
5057700
Merge remote-tracking branch 'origin/develop' into new/location-sharing
Sep 24, 2025
1b35e25
feat: static & live location sharing (draft)
Sep 25, 2025
72dc234
action: organized translations
yiweigao0226 Sep 25, 2025
5450781
feat: static & live location sharing (draft)
Sep 29, 2025
6f44e91
action: organized translations
yiweigao0226 Sep 29, 2025
a991844
feat: static & live location sharing (draft)
Sep 29, 2025
db4f0f3
feat: static & live location sharing (draft)
Sep 29, 2025
3ef9f97
action: organized translations
yiweigao0226 Sep 29, 2025
f59c26d
feat: static & live location sharing (draft)
Sep 30, 2025
fdd3834
feat: static & live location sharing (draft)
Sep 30, 2025
2152119
feat: static & live location sharing (draft)
yiweigao0226 Sep 30, 2025
c4e2514
password show/hide
yiweigao0226 Apr 1, 2018
f4b2aec
[FIX] Bottom border style on DirectoryView (#1963)
yiweigao0226 Apr 1, 2020
bd47d05
feat: static & live location sharing (draft)
yiweigao0226 Oct 3, 2025
366cd18
feat: static & live location sharing (draft)
yiweigao0226 Oct 13, 2025
23ae666
Merge branch 'develop' into new/location-sharing
yiweigao0226 Oct 13, 2025
7cc84af
Merge branch 'RocketChat:develop' into new/location-sharing
yiweigao0226 Oct 14, 2025
43c0adc
feat: static & live location sharing (draft)
yiweigao0226 Oct 14, 2025
8625c64
Merge branch 'develop' into new/location-sharing
yiweigao0226 Oct 20, 2025
8712efc
Merge branch 'develop' into new/location-sharing
yiweigao0226 Oct 22, 2025
90e3452
Merge branch 'RocketChat:develop' into new/location-sharing
yiweigao0226 Oct 28, 2025
1311bd5
feat: static & live location sharing (draft)
yiweigao0226 Oct 28, 2025
320705f
action: organized translations
yiweigao0226 Oct 28, 2025
ad3c8dc
feat: static & live location sharing (draft)
yiweigao0226 Oct 28, 2025
03af849
feat: static & live location sharing (draft)
yiweigao0226 Oct 28, 2025
f17aa5b
action: organized translations
yiweigao0226 Oct 28, 2025
3d83f5e
feat: static & live location sharing (draft)
yiweigao0226 Oct 28, 2025
3eff38a
feat: static & live location sharing (draft)
yiweigao0226 Oct 30, 2025
95ec9cd
action: organized translations
yiweigao0226 Oct 30, 2025
ba6ca43
feat: static & live location sharing (draft)
yiweigao0226 Oct 30, 2025
d695613
feat: static & live location sharing (draft)
yiweigao0226 Oct 31, 2025
289bcfe
feat: static & live location sharing (draft)
yiweigao0226 Oct 31, 2025
8bc29d2
feat: static & live location sharing (draft)
yiweigao0226 Oct 31, 2025
6ea1090
feat: static & live location sharing (draft)
yiweigao0226 Oct 31, 2025
1de7572
feat: static & live location sharing (draft)
yiweigao0226 Oct 31, 2025
c37b1fa
Fix typo in coords property from 'lng' to 'log'
yiweigao0226 Oct 31, 2025
a10295e
Fix typo in coords property from 'log' to 'lon'
yiweigao0226 Oct 31, 2025
4e7a86f
Add tests for CurrentLocationCard and LiveLocationCard
yiweigao0226 Nov 1, 2025
c064b71
Merge branch 'develop' into new/location-sharing
yiweigao0226 Nov 3, 2025
d458659
action: organized translations
yiweigao0226 Nov 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@
<!-- 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" />

<application
android:name="chat.rocket.reactnative.MainApplication"
android:allowBackup="false"
Expand Down Expand Up @@ -87,6 +95,12 @@
<meta-data
android:name="com.bugsnag.android.API_KEY"
android:value="${BugsnagAPIKey}" />

<!-- Foreground location service declaration for Android 10+ compliance -->
<service
android:name=".LocationService"
android:exported="false"
android:foregroundServiceType="location" />
</application>

<queries>
Expand Down
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
}
}
Empty file.
3 changes: 2 additions & 1 deletion app.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"name": "RocketChatRN"
"name": "RocketChatRN",
"plugins": ["expo-web-browser"]
}
188 changes: 176 additions & 12 deletions app/containers/MessageComposer/components/Buttons/ActionsButton.tsx
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';
Expand All @@ -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';
Expand All @@ -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;
Expand All @@ -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' }
]);
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

@coderabbitai coderabbitai bot Sep 30, 2025

Choose a reason for hiding this comment

The 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 @ts-ignore on line 151 indicates missing type definitions for LiveLocationPreviewModal in the navigation param list.

Ensure that the navigation types (e.g., in app/stacks/types.ts or app/stacks/MasterDetailStack/types.ts) include:

LiveLocationPreviewModal: {
  rid: string;
  tmid?: string;
  provider: MapProviderName;
  googleKey?: string;
  osmKey?: string;
  liveLocationId?: string;
  ownerName?: string;
  isTracking?: boolean;
};

This will eliminate the need for @ts-ignore and provide type safety.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolve this too

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


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 });
Expand Down
13 changes: 11 additions & 2 deletions app/containers/message/Components/Attachments/Attachments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@ import Audio from './Audio';
import Video from './Video';
import CollapsibleQuote from './CollapsibleQuote';
import AttachedActions from './AttachedActions';
import LiveLocationAttachment from './LiveLocationAttachment';
import MessageContext from '../../Context';
import { type IMessageAttachments } from '../../interfaces';
import { type IAttachment } from '../../../../definitions';
import { getMessageFromAttachment } from '../../utils';

const removeQuote = (file?: IAttachment) =>
file?.image_url || file?.audio_url || file?.video_url || (file?.actions?.length || 0) > 0 || file?.collapsed;
file?.image_url ||
file?.audio_url ||
file?.video_url ||
(file?.actions?.length || 0) > 0 ||
file?.collapsed ||
file?.type === 'live-location';

const Attachments: React.FC<IMessageAttachments> = React.memo(
({ attachments, timeFormat, showAttachment, getCustomEmoji, author }: IMessageAttachments) => {
({ attachments, timeFormat, showAttachment, getCustomEmoji, author, id, rid }: IMessageAttachments) => {
'use memo';

const { translateLanguage } = useContext(MessageContext);
Expand Down Expand Up @@ -68,6 +74,9 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
return <CollapsibleQuote key={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} />;
}

if (file.type === 'live-location' && file.live) {
return <LiveLocationAttachment key={`live-location-${index}`} attachment={file} messageId={id} roomId={rid} />;
}
return null;
});
return <View style={{ gap: 4 }}>{attachmentsElements}</View>;
Expand Down
Loading