diff --git a/app/package-lock.json b/app/package-lock.json
index 45b9825..9d86f91 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -23,6 +23,9 @@
"react-native": "0.81.5",
"react-native-safe-area-context": "^5.8.0",
"react-native-web": "^0.21.2"
+ },
+ "devDependencies": {
+ "typescript": "^5.9.3"
}
},
"node_modules/@0no-co/graphql.web": {
@@ -7871,6 +7874,20 @@
"node": ">=8"
}
},
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
"node_modules/ua-parser-js": {
"version": "1.0.41",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz",
diff --git a/app/package.json b/app/package.json
index ee27c52..5c7522c 100644
--- a/app/package.json
+++ b/app/package.json
@@ -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",
@@ -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"
+ }
}
diff --git a/app/scripts/check-jsx.mjs b/app/scripts/check-jsx.mjs
new file mode 100644
index 0000000..9d2de5f
--- /dev/null
+++ b/app/scripts/check-jsx.mjs
@@ -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.`);
diff --git a/app/src/screens/CalorieScreen.jsx b/app/src/screens/CalorieScreen.jsx
index 439e76a..66bde58 100644
--- a/app/src/screens/CalorieScreen.jsx
+++ b/app/src/screens/CalorieScreen.jsx
@@ -266,7 +266,8 @@ export default function CalorieScreen({ onOpenDrawer, avatarLabel }) {
Macro targets
{
- setMeals(baseMeals);
+ setMeals(baseMeals);
+ }}>
Reset day
{
diff --git a/app/src/screens/HangoutScreen.jsx b/app/src/screens/HangoutScreen.jsx
index a47f6c3..e10bb9d 100644
--- a/app/src/screens/HangoutScreen.jsx
+++ b/app/src/screens/HangoutScreen.jsx
@@ -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');
@@ -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 || [];
@@ -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();
}
@@ -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) {
@@ -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;
}
const message = friendName
@@ -262,21 +268,18 @@ 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);
@@ -284,8 +287,7 @@ export default function HangoutScreen({ onOpenDrawer, avatarLabel, sessionToken,
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)));
@@ -446,6 +448,15 @@ export default function HangoutScreen({ onOpenDrawer, avatarLabel, sessionToken,
{activeRoom.roomType} • {activeRoom.ownerDisplayName} • {activeRoom.participantCount} in room
{activeRoom.joinLink}
+ {statusMessage ? (
+
+ {statusMessage}
+ ) : null}
{getInitials(userName)}
@@ -629,8 +640,8 @@ export default function HangoutScreen({ onOpenDrawer, avatarLabel, sessionToken,
Prof. Deshmukh: Let's revise normalization before we solve the next DBMS problem.
) : null}
- {reactionBurst ? (
- {reactionBurst.icon}
+ {reactionBurst ? (
+
{reactionBurst.label}
) : null}
@@ -661,12 +672,12 @@ export default function HangoutScreen({ onOpenDrawer, avatarLabel, sessionToken,
{[
- { 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) => ( sendReaction(reaction.icon, reaction.label)}>
- {reaction.icon}
+
{reaction.label}
))}
@@ -838,12 +849,15 @@ function MeetingPanelSheet({ visible, panel, participants, messages, activityFee
) : null}
{panel === 'settings' ? (
+ {!isHost ? (
+ Only the host can change room-wide settings.
+ ) : null}
onUpdateSettings({ ...settings, noiseCancellation: value })}/>
onUpdateSettings({ ...settings, lowLightMode: value })}/>
onUpdateSettings({ ...settings, mirrorSelfView: value })}/>
- onUpdateSettings({ ...settings, allowChat: value })}/>
- onUpdateSettings({ ...settings, allowGuestScreenShare: value })}/>
- onUpdateSettings({ ...settings, waitingRoomEnabled: value })}/>
+ onUpdateSettings({ ...settings, allowChat: value })}/>
+ onUpdateSettings({ ...settings, allowGuestScreenShare: value })}/>
+ onUpdateSettings({ ...settings, waitingRoomEnabled: value })}/>
Layout
@@ -881,10 +895,10 @@ function MeetingPanelSheet({ visible, panel, participants, messages, activityFee
);
}
-function SettingsRow({ label, value, onValueChange, }) {
- return (
+function SettingsRow({ label, value, disabled = false, onValueChange, }) {
+ return (
{label}
-
+
);
}
const panelTitleMap = {
@@ -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,
diff --git a/app/tsconfig.json b/app/tsconfig.json
new file mode 100644
index 0000000..05207c6
--- /dev/null
+++ b/app/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "allowJs": false,
+ "checkJs": false,
+ "jsx": "react-native",
+ "noEmit": true,
+ "skipLibCheck": true,
+ "strict": false
+ },
+ "files": ["types/empty.d.ts"]
+}
diff --git a/app/types/empty.d.ts b/app/types/empty.d.ts
new file mode 100644
index 0000000..cb0ff5c
--- /dev/null
+++ b/app/types/empty.d.ts
@@ -0,0 +1 @@
+export {};