Skip to content
Merged
Changes from all commits
Commits
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
114 changes: 99 additions & 15 deletions app/src/screens/HangoutScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ type PersistedHangoutState = {
activeRoom: HangoutRoom | null;
};

type StatusTone = 'neutral' | 'info' | 'success' | 'error';
type LoadingAction = 'idle' | 'refreshing' | 'creating' | 'joining' | 'opening' | 'sharing';

const friends: Friend[] = [
{ name: 'Ananya', status: 'online', note: 'Ready for revision' },
{ name: 'Isha', status: 'online', note: 'Can join in 2 min' },
Expand Down Expand Up @@ -90,7 +93,8 @@ export default function HangoutScreen({
const [roomType, setRoomType] = useState('Study');
const [joinInput, setJoinInput] = useState('');
const [statusMessage, setStatusMessage] = useState('Create a room or paste a room link to join one.');
const [loading, setLoading] = useState(false);
const [statusTone, setStatusTone] = useState<StatusTone>('neutral');
const [loadingAction, setLoadingAction] = useState<LoadingAction>('idle');
Comment on lines +96 to +97

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Keep banner tone and copy in one update path.

statusTone and statusMessage are now independent states, but several lobby actions still only update the message, like the mic/camera preview toggles. After an error banner, the next neutral message can still render with error styling. A small setStatus(tone, message) helper—or a single { tone, message } state object—would prevent that drift.

Also applies to: 561-588

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/screens/HangoutScreen.tsx` around lines 96 - 97, statusTone and
statusMessage are updated independently which allows UI drift (e.g., an error
tone persisting while the message changes); consolidate them by replacing the
two separate states with a single state object (e.g., status: { tone:
StatusTone, message: string }) or add a unified setter setStatus(tone, message)
that updates both together; update all usages (including handlers for mic/camera
preview toggles and the areas referenced around the start of the file and lines
~561-588) to call the new setStatus or to read from the single status object and
remove direct calls to setStatusTone/setStatusMessage.


const [meetingOpen, setMeetingOpen] = useState(false);
const [meetingPanel, setMeetingPanel] = useState<MeetingPanel>('none');
Expand Down Expand Up @@ -206,11 +210,20 @@ export default function HangoutScreen({
}, [activeRoom, hydrated, joinInput, roomName, roomType, setPersistedState]);

async function refreshRooms() {
setLoadingAction('refreshing');
const result = await listRooms();
setLoadingAction('idle');
Comment on lines +213 to +215

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t block the primary CTAs behind the room-list refresh.

The mount refresh sets loadingAction = 'refreshing', and both buttons disable on any non-idle action. That means a slow listRooms() call can prevent the user from creating or joining a room even though neither flow depends on the room list. Track room-list refresh separately, or exclude 'refreshing' from these disabled checks.

Also applies to: 528-530, 551-553

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/screens/HangoutScreen.tsx` around lines 213 - 215, The current mount
refresh uses setLoadingAction('refreshing') and listRooms(), which blocks
primary CTAs because the buttons are disabled for any non-'idle' loadingAction;
change the logic so room-list refresh is tracked separately (e.g., introduce a
new state like roomListLoading or isRefreshingRooms) or update the
button-disabled checks to ignore the 'refreshing' value. Specifically, stop
using setLoadingAction('refreshing') for the listRooms() call in the mount flow
and instead set/clear a dedicated flag around listRooms(), and update any places
that check loadingAction (the primary CTA disable logic near the button render
code) to not disable when loadingAction === 'refreshing' or to use the new
room-list-specific flag; apply the same fix for the other occurrences referenced
(around the calls that set loadingAction to 'refreshing' at the lines you
noted).

if (result.ok) {
setRooms(result.rooms);
setStatusTone('info');
setStatusMessage(
result.rooms.length
? `${result.rooms.length} live room${result.rooms.length === 1 ? '' : 's'} ready to browse.`
: 'No live rooms yet. Create the first one or join from a Sentri link.'
);
Comment on lines +218 to +223

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep the post-create/join refresh from clobbering the actual outcome.

refreshRooms() always rewrites the banner on success, so the success copy from Lines 270-271 and 302-303 is immediately replaced by the generic room-count message. If that follow-up refresh fails, the lobby even shows an error although the room creation/join already succeeded. Make the sync refresh silent, or let refreshRooms() opt out of touching statusTone / statusMessage.

Also applies to: 270-272, 302-304

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/screens/HangoutScreen.tsx` around lines 218 - 223, The
post-create/join success message gets overwritten by the follow-up
refreshRooms() call; update refreshRooms (or its callers) so the sync refresh is
"silent" by default or accepts an option like { preserveStatus: true } to avoid
touching setStatusTone/setStatusMessage, and update the post-create/join paths
that call refreshRooms() (the handlers that setStatusTone/setStatusMessage on
successful create/join) to call refreshRooms({ preserveStatus: true }) or
otherwise skip status updates; ensure refreshRooms only updates the status when
preserveStatus is false (or when an explicit status payload is provided) so a
failed refresh won't clobber the actual create/join outcome.

return;
}
setStatusTone('error');
setStatusMessage(result.message);
}

Expand All @@ -233,44 +246,49 @@ export default function HangoutScreen({

async function handleCreateRoom() {
if (!sessionToken) {
setStatusTone('error');
setStatusMessage('Login first so Sentri can create a room under your account.');
return;
}

setLoading(true);
setLoadingAction('creating');
const result = await createRoom(sessionToken, {
roomName: roomName.trim() || 'Sentri Room',
roomType,
});
setLoading(false);
setLoadingAction('idle');

if (!result.ok) {
setStatusTone('error');
setStatusMessage(result.message);
return;
}

setActiveRoom(result.room);
setJoinInput(result.room.joinLink);
seedMeetingRoom(result.room);
setStatusTone('success');
setStatusMessage(`Room ${result.room.roomCode} is live and ready to share.`);
await refreshRooms();
}

async function handleJoinByCode(rawValue?: string, fromIncomingLink = false) {
const code = extractRoomCode(rawValue ?? joinInput);
if (!code) {
setStatusTone('error');
setStatusMessage('Paste a Sentri room link or room code first.');
if (fromIncomingLink) {
onConsumeIncomingRoomCode();
}
return;
}

setLoading(true);
setLoadingAction('joining');
const result = await joinRoom(code, userName);
setLoading(false);
setLoadingAction('idle');

if (!result.ok) {
setStatusTone('error');
setStatusMessage(result.message);
if (fromIncomingLink) {
onConsumeIncomingRoomCode();
Expand All @@ -281,6 +299,7 @@ export default function HangoutScreen({
setActiveRoom(result.room);
setJoinInput(result.room.roomCode);
seedMeetingRoom(result.room);
setStatusTone('success');
setStatusMessage(`Joined ${result.room.roomName}.`);
await refreshRooms();
if (fromIncomingLink) {
Expand All @@ -289,23 +308,26 @@ export default function HangoutScreen({
}

async function handleOpenRoom(roomCode: string) {
setLoading(true);
setLoadingAction('opening');
const result = await getRoom(roomCode);
setLoading(false);
setLoadingAction('idle');

if (!result.ok) {
setStatusTone('error');
setStatusMessage(result.message);
return;
}

setActiveRoom(result.room);
setJoinInput(result.room.roomCode);
seedMeetingRoom(result.room);
setStatusTone('info');
setStatusMessage(`${result.room.roomName} is ready.`);
}

async function handleShareRoom(friendName?: string) {
if (!activeRoom) {
setStatusTone('error');
setStatusMessage('Create or join a room before sharing it.');
return;
}
Expand All @@ -314,22 +336,27 @@ export default function HangoutScreen({
? `${activeRoomShareText}\nInviting ${friendName} from Sentri.`
: activeRoomShareText;

setLoadingAction('sharing');
await Share.share({
title: activeRoom.roomName,
message,
});
setLoadingAction('idle');
setStatusTone('success');
setStatusMessage(friendName ? `Share sheet opened for ${friendName}.` : 'Share sheet opened.');
Comment on lines +339 to 346

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

head -n 350 app/src/screens/HangoutScreen.tsx | tail -n 20

Repository: SahilKumar75/sentri

Length of output: 618


🌐 Web query:

React Native Share.share() API return value documentation

💡 Result:

The React Native Share.share API returns a Promise that resolves differently by platform. On iOS: Resolves with an object containing 'action' ('sharedAction' or 'dismissedAction') and optionally 'activityType'. On Android: Always resolves with action: 'sharedAction' (indicating the share sheet opened successfully, not necessarily that sharing completed). Example usage: Share.share({message: 'content'}) .then(result => console.log(result)) .catch(err => console.log(err)); Note: react-native-share (third-party package) uses Share.open with different behavior, especially unreliable success detection on Android. The query refers to core React Native API. Official docs updated as of 2026 confirm this behavior remains unchanged.

Citations:


Check the Share.share() return value before marking the action as successful.

React Native's Share.share() resolves with different actions by platform: on iOS it returns dismissedAction when the user closes the sheet without sharing, while on Android it always returns sharedAction (indicating only that the sheet opened, not that sharing completed). The current code ignores this return value and always displays a success status, which incorrectly suggests the share succeeded when the user dismissed the sheet on iOS. Only set the success status when the action indicates the share actually occurred.

Suggested fix
-    await Share.share({
+    const shareResult = await Share.share({
       title: activeRoom.roomName,
       message,
     });
     setLoadingAction('idle');
-    setStatusTone('success');
-    setStatusMessage(friendName ? `Share sheet opened for ${friendName}.` : 'Share sheet opened.');
+    if (shareResult.action === Share.sharedAction) {
+      setStatusTone('success');
+      setStatusMessage(friendName ? `Shared with ${friendName}.` : 'Room details shared.');
+    } else {
+      setStatusTone('info');
+      setStatusMessage('Share sheet dismissed.');
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setLoadingAction('sharing');
await Share.share({
title: activeRoom.roomName,
message,
});
setLoadingAction('idle');
setStatusTone('success');
setStatusMessage(friendName ? `Share sheet opened for ${friendName}.` : 'Share sheet opened.');
setLoadingAction('sharing');
const shareResult = await Share.share({
title: activeRoom.roomName,
message,
});
setLoadingAction('idle');
if (shareResult.action === Share.sharedAction) {
setStatusTone('success');
setStatusMessage(friendName ? `Shared with ${friendName}.` : 'Room details shared.');
} else {
setStatusTone('info');
setStatusMessage('Share sheet dismissed.');
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/screens/HangoutScreen.tsx` around lines 339 - 346, The Share.share()
call result is ignored causing setStatusTone('success')/setStatusMessage(...) to
be shown even when the user dismissed the sheet; update the share flow in the
handler that calls Share.share (the await Share.share(...) block near
setLoadingAction) to inspect the returned result.action (compare to
Share.sharedAction and Share.dismissedAction), only set status to success when
result.action indicates sharing occurred, set an appropriate dismissed or
cancelled tone/message when dismissed, and ensure setLoadingAction('idle') is
still called in all code paths; use activeRoom.roomName and friendName to build
messages as before.

}

function handleEnterMeeting() {
if (!activeRoom) {
setStatusTone('error');
setStatusMessage('Open a room first.');
return;
}
if (!meetingParticipants.length) {
seedMeetingRoom(activeRoom);
}
setMeetingOpen(true);
setStatusTone('info');
setStatusMessage(`Inside ${activeRoom.roomName}.`);
}

Expand All @@ -339,6 +366,7 @@ export default function HangoutScreen({
setShareScreenOn(false);
setRecordingOn(false);
setFocusedParticipantId(meetingParticipants[1]?.id ?? meetingParticipants[0]?.id ?? null);
setStatusTone('info');
setStatusMessage(activeRoom ? `Left ${activeRoom.roomName}. Room is still active.` : 'Left the meeting.');
}

Expand Down Expand Up @@ -497,11 +525,13 @@ export default function HangoutScreen({
</View>
<View style={styles.actionRow}>
<Pressable
style={[styles.actionButton, styles.actionButtonFilled, loading && styles.disabledButton]}
style={[styles.actionButton, styles.actionButtonFilled, loadingAction !== 'idle' && styles.disabledButton]}
onPress={() => void handleCreateRoom()}
disabled={loading}
disabled={loadingAction !== 'idle'}
>
<Text style={styles.actionFilledText}>{loading ? 'Please wait' : 'Create room'}</Text>
<Text style={styles.actionFilledText}>
{loadingAction === 'creating' ? 'Creating room' : 'Create room'}
</Text>
</Pressable>
</View>
</View>
Expand All @@ -518,15 +548,43 @@ export default function HangoutScreen({
autoCorrect={false}
/>
<Pressable
style={[styles.actionButton, styles.actionButtonGhost, loading && styles.disabledButton]}
style={[styles.actionButton, styles.actionButtonGhost, loadingAction !== 'idle' && styles.disabledButton]}
onPress={() => void handleJoinByCode()}
disabled={loading}
disabled={loadingAction !== 'idle'}
>
<Text style={styles.actionGhostText}>{loading ? 'Please wait' : 'Join room'}</Text>
<Text style={styles.actionGhostText}>
{loadingAction === 'joining' ? 'Joining room' : 'Join room'}
</Text>
</Pressable>
</View>

<View style={styles.statusBanner}>
<View
style={[
styles.statusBanner,
statusTone === 'success' && styles.statusBannerSuccess,
statusTone === 'error' && styles.statusBannerError,
statusTone === 'info' && styles.statusBannerInfo,
]}
>
<Ionicons
name={
statusTone === 'success'
? 'checkmark-circle'
: statusTone === 'error'
? 'alert-circle'
: statusTone === 'info'
? 'information-circle'
: 'radio-button-on'
}
size={16}
color={
statusTone === 'error'
? '#B3261E'
: statusTone === 'success'
? theme.colors.accentStrong
: theme.colors.text
}
/>
<Text style={styles.statusBannerText}>{statusMessage}</Text>
</View>

Expand Down Expand Up @@ -585,9 +643,19 @@ export default function HangoutScreen({
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>Live rooms</Text>
<Pressable onPress={() => void refreshRooms()}>
<Text style={styles.sectionMeta}>Refresh</Text>
<Text style={styles.sectionMeta}>
{loadingAction === 'refreshing' ? 'Refreshing…' : 'Refresh'}
</Text>
</Pressable>
</View>
{loadingAction === 'refreshing' && rooms.length === 0 ? (
<View style={styles.emptyRooms}>
<Text style={styles.emptyRoomsTitle}>Refreshing room list</Text>
<Text style={styles.emptyRoomsBody}>
Sentri is checking for live study rooms and shared hangouts right now.
</Text>
</View>
) : null}
Comment on lines +651 to +658

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

The new refreshing placeholder now renders together with the old empty state.

When rooms.length === 0 and loadingAction === 'refreshing', this block renders, and the existing rooms.length === 0 fallback below still renders too. The result is two contradictory cards at once. Gate the final empty-state branch with loadingAction !== 'refreshing'.

Suggested fix
-          {rooms.length === 0 ? (
+          {loadingAction !== 'refreshing' && rooms.length === 0 ? (
             <View style={styles.emptyRooms}>
               <Text style={styles.emptyRoomsTitle}>No rooms yet</Text>
               <Text style={styles.emptyRoomsBody}>
                 Create the first study room or join one from a Sentri link.
               </Text>
             </View>
           ) : null}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/screens/HangoutScreen.tsx` around lines 651 - 658, The new
"refreshing" placeholder is rendered alongside the existing empty-state card
when rooms.length === 0; update the final empty-state fallback in HangoutScreen
(the branch that renders when rooms.length === 0) to also check loadingAction
!== 'refreshing' so the empty-state UI is suppressed during refreshing. Locate
the conditional that renders the existing empty rooms card (uses rooms.length
and styles.emptyRooms / styles.emptyRoomsTitle / styles.emptyRoomsBody) and add
the extra guard against loadingAction === 'refreshing' so only one placeholder
shows at a time.

{rooms.map((room) => (
<Pressable key={room.roomCode} style={styles.roomCard} onPress={() => void handleOpenRoom(room.roomCode)}>
<View style={styles.roomBadge}>
Expand Down Expand Up @@ -1344,13 +1412,29 @@ const styles = StyleSheet.create({
marginTop: 14,
borderRadius: 18,
backgroundColor: theme.colors.surfaceAlt,
borderWidth: 1,
borderColor: theme.colors.line,
paddingHorizontal: 14,
paddingVertical: 12,
flexDirection: 'row',
alignItems: 'center',
gap: 10,
},
statusBannerSuccess: {
backgroundColor: theme.colors.accentSoft,
},
statusBannerError: {
backgroundColor: '#FDEDED',
borderColor: '#F5C2C0',
},
statusBannerInfo: {
backgroundColor: '#EEF3FD',
},
statusBannerText: {
color: theme.colors.text,
fontSize: 13,
fontWeight: '700',
flex: 1,
},
linkCard: {
marginTop: 16,
Expand Down