Skip to content
Merged
Show file tree
Hide file tree
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
17 changes: 17 additions & 0 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"start": "expo start",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web"
"web": "expo start --web",
"lint": "node scripts/check-jsx.mjs"
},
"dependencies": {
"@react-native-async-storage/async-storage": "2.2.0",
Expand All @@ -25,5 +26,8 @@
"react-native-safe-area-context": "^5.8.0",
"react-native-web": "^0.21.2"
},
"private": true
"private": true,
"devDependencies": {
"typescript": "^5.9.3"
}
}
35 changes: 35 additions & 0 deletions app/scripts/check-jsx.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { parse } from '@babel/parser';
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join } from 'node:path';

const roots = ['App.jsx', 'index.js', 'src'];
const extensions = new Set(['.js', '.jsx']);
const files = [];

function collect(path) {
const stat = statSync(path);
if (stat.isDirectory()) {
for (const entry of readdirSync(path)) {
collect(join(path, entry));
}
return;
}

const extension = path.slice(path.lastIndexOf('.'));
if (extensions.has(extension)) {
files.push(path);
}
}

for (const root of roots) {
collect(root);
}

for (const file of files) {
parse(readFileSync(file, 'utf8'), {
sourceType: 'module',
plugins: ['jsx'],
});
}

console.log(`Checked ${files.length} JavaScript files.`);
3 changes: 2 additions & 1 deletion app/src/screens/CalorieScreen.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,8 @@ export default function CalorieScreen({ onOpenDrawer, avatarLabel }) {
<Text style={styles.sectionTitleDark}>Macro targets</Text>
<View style={styles.headerActions}>
<Pressable style={styles.secondaryButton} onPress={() => {
setMeals(baseMeals);
setMeals(baseMeals);
}}>
<Text style={styles.secondaryButtonText}>Reset day</Text>
</Pressable>
<Pressable style={styles.secondaryButton} onPress={() => {
Expand Down
71 changes: 44 additions & 27 deletions app/src/screens/HangoutScreen.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export default function HangoutScreen({ onOpenDrawer, avatarLabel, sessionToken,
const [roomNameError, setRoomNameError] = useState('');
const [searchQuery, setSearchQuery] = useState('');
const [activeFilter, setActiveFilter] = useState('All');
const [statusMessage, setStatusMessage] = useState('');
const [statusTone, setStatusTone] = useState('info');
const [toast, setToast] = useState({ visible: false, message: '', type: 'info' });
const [roomFetchError, setRoomFetchError] = useState(null);
const [loadingAction, setLoadingAction] = useState('idle');
Expand Down Expand Up @@ -111,6 +113,11 @@ export default function HangoutScreen({ onOpenDrawer, avatarLabel, sessionToken,
return activeRoom.ownerDisplayName.trim().toLowerCase() === userName.trim().toLowerCase();
}, [activeRoom, userName]);
const canShareScreen = isHost || meetingSettings.allowGuestScreenShare;

function showStatus(message, tone = 'info') {
setStatusTone(tone);
setStatusMessage(message);
}

const filteredRooms = useMemo(() => {
let result = rooms || [];
Expand Down Expand Up @@ -200,6 +207,7 @@ export default function HangoutScreen({ onOpenDrawer, avatarLabel, sessionToken,
setActiveRoom(result.room);
setJoinInput(result.room.joinLink);
seedMeetingRoom(result.room);
showStatus(`${result.room.roomName} is ready to join.`, 'success');
setToast({ visible: true, message: `Room ${result.room.roomCode} is live.`, type: 'success' });
await refreshRooms();
}
Expand All @@ -226,6 +234,7 @@ export default function HangoutScreen({ onOpenDrawer, avatarLabel, sessionToken,
setActiveRoom(result.room);
setJoinInput(result.room.roomCode);
seedMeetingRoom(result.room);
showStatus(`${result.room.roomName} is ready to join.`, 'success');
setToast({ visible: true, message: `Joined ${result.room.roomName}.`, type: 'success' });
await refreshRooms();
if (fromIncomingLink) {
Expand All @@ -237,20 +246,17 @@ export default function HangoutScreen({ onOpenDrawer, avatarLabel, sessionToken,
const result = await getRoom(roomCode);
setLoadingAction('idle');
if (!result.ok) {
setStatusTone('error');
setStatusMessage(result.message);
showStatus(result.message, 'error');
return;
}
setActiveRoom(result.room);
setJoinInput(result.room.roomCode);
seedMeetingRoom(result.room);
setStatusTone('info');
setStatusMessage(`${result.room.roomName} is ready.`);
showStatus(`${result.room.roomName} is ready.`, 'info');
}
async function handleShareRoom(friendName) {
if (!activeRoom) {
setStatusTone('error');
setStatusMessage('Create or join a room before sharing it.');
showStatus('Create or join a room before sharing it.', 'error');
return;
Comment on lines +259 to 260

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 | ⚡ Quick win

Error status won't display—banner requires activeRoom.

This showStatus call sets an error message when there's no active room, but the status banner (lines 451–459) only renders inside the activeRoom conditional block (line 437). Users won't see this error.

🔧 Proposed fix: render banner outside activeRoom conditional

Move the status banner outside the activeRoom check or add a separate error display area that's always visible in the lobby. For example, render the banner before the activeRoom conditional:

         </View>
       </View>

+      {statusMessage ? (
+        <View style={[
+          styles.statusBanner,
+          statusTone === 'success' && styles.statusBannerSuccess,
+          statusTone === 'error' && styles.statusBannerError,
+          statusTone === 'info' && styles.statusBannerInfo,
+        ]}>
+          <Ionicons 
+            name={statusTone === 'error' ? 'warning' : statusTone === 'success' ? 'checkmark-circle' : 'information-circle'} 
+            size={18} 
+            color={statusTone === 'error' ? '`#B3261E`' : theme.colors.accentStrong}
+          />
+          <Text style={styles.statusBannerText}>{statusMessage}</Text>
+        </View>
+      ) : null}
+
       {activeRoom ? (<View style={styles.linkCard}>

Then remove the duplicate banner from inside the activeRoom block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/screens/HangoutScreen.jsx` around lines 259 - 260, The status message
set by showStatus is never visible because the status banner UI is only rendered
inside the activeRoom conditional; move the status banner rendering out of the
activeRoom block (or create a separate always-visible lobby error area) so the
banner displays even when activeRoom is null, then remove the duplicate banner
inside the activeRoom branch; update any references to the banner/component so
showStatus continues to control its content and visibility.

}
const message = friendName
Expand All @@ -262,30 +268,26 @@ export default function HangoutScreen({ onOpenDrawer, avatarLabel, sessionToken,
message,
});
setLoadingAction('idle');
setStatusTone('success');
setStatusMessage(friendName ? `Share sheet opened for ${friendName}.` : 'Share sheet opened.');
showStatus(friendName ? `Share sheet opened for ${friendName}.` : 'Share sheet opened.', 'success');
}
function handleEnterMeeting() {
if (!activeRoom) {
setStatusTone('error');
setStatusMessage('Open a room first.');
showStatus('Open a room first.', 'error');
return;
}
if (!meetingParticipants.length) {
seedMeetingRoom(activeRoom);
}
setMeetingOpen(true);
setStatusTone('info');
setStatusMessage(`Inside ${activeRoom.roomName}.`);
showStatus(`Inside ${activeRoom.roomName}.`, 'info');
}
function handleLeaveMeeting() {
setMeetingOpen(false);
setMeetingPanel('none');
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.');
showStatus(activeRoom ? `Left ${activeRoom.roomName}. Room is still active.` : 'Left the meeting.', 'info');
}
function updateSelfParticipant(updater) {
setMeetingParticipants((current) => current.map((participant, index) => (index === 0 ? updater(participant) : participant)));
Expand Down Expand Up @@ -446,6 +448,15 @@ export default function HangoutScreen({ onOpenDrawer, avatarLabel, sessionToken,
{activeRoom.roomType} • {activeRoom.ownerDisplayName} • {activeRoom.participantCount} in room
</Text>
<Text style={styles.joinLinkText}>{activeRoom.joinLink}</Text>
{statusMessage ? (<View style={[
styles.statusBanner,
statusTone === 'success' && styles.statusBannerSuccess,
statusTone === 'error' && styles.statusBannerError,
statusTone === 'info' && styles.statusBannerInfo,
]}>
<Ionicons name={statusTone === 'error' ? 'warning' : statusTone === 'success' ? 'checkmark-circle' : 'information-circle'} size={18} color={statusTone === 'error' ? '#B3261E' : theme.colors.accentStrong}/>
<Text style={styles.statusBannerText}>{statusMessage}</Text>
</View>) : null}
<View style={styles.previewCard}>
<View style={styles.previewVisual}>
<Text style={styles.previewInitials}>{getInitials(userName)}</Text>
Expand Down Expand Up @@ -629,8 +640,8 @@ export default function HangoutScreen({ onOpenDrawer, avatarLabel, sessionToken,
<Text style={styles.captionText}>Prof. Deshmukh: Let&apos;s revise normalization before we solve the next DBMS problem.</Text>
</View>) : null}

{reactionBurst ? (<View style={styles.reactionBubble}>
<Text style={styles.reactionEmoji}>{reactionBurst.icon}</Text>
{reactionBurst ? (<View style={styles.reactionBubble}>
<Ionicons name={reactionBurst.icon} size={24} color="#FFFFFF"/>
<Text style={styles.reactionLabel}>{reactionBurst.label}</Text>
</View>) : null}
</View>
Expand Down Expand Up @@ -661,12 +672,12 @@ export default function HangoutScreen({ onOpenDrawer, avatarLabel, sessionToken,

<View style={styles.reactionRow}>
{[
{ icon: '👏', label: 'Clap' },
{ icon: '🔥', label: 'Fire' },
{ icon: '👍', label: 'Thumbs up' },
{ icon: '🙌', label: 'Celebrate' },
{ icon: 'hand-left', label: 'Raise hand' },
{ icon: 'checkmark-circle', label: 'Agree' },
{ icon: 'help-circle', label: 'Question' },
{ icon: 'bulb', label: 'Idea' },
].map((reaction) => (<Pressable key={reaction.label} style={styles.reactionChip} onPress={() => sendReaction(reaction.icon, reaction.label)}>
<Text style={styles.reactionChipEmoji}>{reaction.icon}</Text>
<Ionicons name={reaction.icon} size={16} color={theme.colors.darkText}/>
<Text style={styles.reactionChipText}>{reaction.label}</Text>
</Pressable>))}
</View>
Expand Down Expand Up @@ -838,12 +849,15 @@ function MeetingPanelSheet({ visible, panel, participants, messages, activityFee
</ScrollView>) : null}

{panel === 'settings' ? (<ScrollView style={styles.panelScroll} showsVerticalScrollIndicator={false}>
{!isHost ? (<View style={styles.guestNotice}>
<Text style={styles.guestNoticeText}>Only the host can change room-wide settings.</Text>
</View>) : null}
<SettingsRow label="Noise cancellation" value={settings.noiseCancellation} onValueChange={(value) => onUpdateSettings({ ...settings, noiseCancellation: value })}/>
<SettingsRow label="Low light mode" value={settings.lowLightMode} onValueChange={(value) => onUpdateSettings({ ...settings, lowLightMode: value })}/>
<SettingsRow label="Mirror self view" value={settings.mirrorSelfView} onValueChange={(value) => onUpdateSettings({ ...settings, mirrorSelfView: value })}/>
<SettingsRow label="Allow in-call chat" value={settings.allowChat} onValueChange={(value) => onUpdateSettings({ ...settings, allowChat: value })}/>
<SettingsRow label="Allow guest screen share" value={settings.allowGuestScreenShare} onValueChange={(value) => onUpdateSettings({ ...settings, allowGuestScreenShare: value })}/>
<SettingsRow label="Waiting room" value={settings.waitingRoomEnabled} onValueChange={(value) => onUpdateSettings({ ...settings, waitingRoomEnabled: value })}/>
<SettingsRow label="Allow in-call chat" value={settings.allowChat} disabled={!isHost} onValueChange={(value) => onUpdateSettings({ ...settings, allowChat: value })}/>
<SettingsRow label="Allow guest screen share" value={settings.allowGuestScreenShare} disabled={!isHost} onValueChange={(value) => onUpdateSettings({ ...settings, allowGuestScreenShare: value })}/>
<SettingsRow label="Waiting room" value={settings.waitingRoomEnabled} disabled={!isHost} onValueChange={(value) => onUpdateSettings({ ...settings, waitingRoomEnabled: value })}/>

<Text style={styles.settingsLabel}>Layout</Text>
<View style={styles.settingsChipRow}>
Expand Down Expand Up @@ -881,10 +895,10 @@ function MeetingPanelSheet({ visible, panel, participants, messages, activityFee
</View>
</Modal>);
}
function SettingsRow({ label, value, onValueChange, }) {
return (<View style={styles.settingsRow}>
function SettingsRow({ label, value, disabled = false, onValueChange, }) {
return (<View style={[styles.settingsRow, disabled && styles.settingsRowDisabled]}>
<Text style={styles.settingsText}>{label}</Text>
<Switch value={value} onValueChange={onValueChange} trackColor={{ false: '#DADCE0', true: theme.colors.accentSoft }} thumbColor={value ? theme.colors.accentStrong : '#FFFFFF'}/>
<Switch value={value} disabled={disabled} onValueChange={onValueChange} trackColor={{ false: '#DADCE0', true: theme.colors.accentSoft }} thumbColor={value ? theme.colors.accentStrong : '#FFFFFF'}/>
</View>);
}
const panelTitleMap = {
Expand Down Expand Up @@ -1923,6 +1937,9 @@ const styles = StyleSheet.create({
borderBottomWidth: 1,
borderBottomColor: theme.colors.line,
},
settingsRowDisabled: {
opacity: 0.55,
},
settingsText: {
color: theme.colors.text,
fontSize: 15,
Expand Down
11 changes: 11 additions & 0 deletions app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"allowJs": false,
"checkJs": false,
"jsx": "react-native",
"noEmit": true,
"skipLibCheck": true,
"strict": false
},
"files": ["types/empty.d.ts"]
}
1 change: 1 addition & 0 deletions app/types/empty.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
Loading