Skip to content

Commit 4d30ad1

Browse files
author
Ryan Vogel
committed
feat: enhance mobile voice build and UI layout
- Updated AGENTS.md with new build commands for development and production. - Added a new script in package.json for starting Expo with a specific hostname. - Improved layout handling in DictationScreen by adding dynamic height calculations for dropdown menus and footers. - Introduced new state variables to manage menu list heights and footer heights for better UI responsiveness. - Enhanced QR code generation for mobile pairing in the serve command, allowing for a connect QR option without starting the server.
1 parent c90640e commit 4d30ad1

5 files changed

Lines changed: 185 additions & 61 deletions

File tree

packages/mobile-voice/AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,9 @@ Example shape:
175175
- If behavior could break startup, run `bunx expo export --platform ios --clear`.
176176
- Confirm no accidental config side effects were introduced.
177177
- Summarize what was verified on-device vs only in tooling.
178+
179+
180+
- Dev build (internal/dev client):
181+
- bunx eas build --profile development --platform ios
182+
- Production build + auto-submit:
183+
- bunx eas build --profile production --platform ios --auto-submit

packages/mobile-voice/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"version": "1.0.0",
55
"scripts": {
66
"start": "expo start",
7+
"expo:start": "REACT_NATIVE_PACKAGER_HOSTNAME=exos.husky-tilapia.ts.net expo start --dev-client --clear --host lan",
78
"relay": "echo 'Use packages/apn-relay for APNs relay server'",
89
"relay:legacy": "node ./relay/opencode-relay.mjs",
910
"reset-project": "node ./scripts/reset-project.js",

packages/mobile-voice/src/app/index.tsx

Lines changed: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ const WAVEFORM_ROWS = 5
4848
const WAVEFORM_CELL_SIZE = 8
4949
const WAVEFORM_CELL_GAP = 2
5050
const DROPDOWN_VISIBLE_ROWS = 6
51+
const DROPDOWN_ROW_HEIGHT = 42
52+
const SERVER_MENU_SECTION_HEIGHT = 56
53+
const SERVER_MENU_ENTRY_HEIGHT = 36
54+
const SERVER_MENU_FOOTER_HEIGHT = 28
5155
// If the press duration is shorter than this, treat it as a tap (toggle)
5256
const TAP_THRESHOLD_MS = 300
5357
const SERVER_STATE_FILE = `${FileSystem.documentDirectory}mobile-voice-servers.json`
@@ -591,6 +595,10 @@ export default function DictationScreen() {
591595
const [readerModeRendered, setReaderModeRendered] = useState(false)
592596
const [dropdownMode, setDropdownMode] = useState<DropdownMode>("none")
593597
const [dropdownRenderMode, setDropdownRenderMode] = useState<Exclude<DropdownMode, "none">>("server")
598+
const [serverMenuListHeight, setServerMenuListHeight] = useState(0)
599+
const [sessionMenuListHeight, setSessionMenuListHeight] = useState(0)
600+
const [serverMenuFooterHeight, setServerMenuFooterHeight] = useState(0)
601+
const [sessionMenuFooterHeight, setSessionMenuFooterHeight] = useState(0)
594602
const [sessionCreateMode, setSessionCreateMode] = useState<"same" | "root" | null>(null)
595603
const [scanOpen, setScanOpen] = useState(false)
596604
const [pairSelectionOpen, setPairSelectionOpen] = useState(false)
@@ -633,6 +641,8 @@ export default function DictationScreen() {
633641
setDropdownMode("none")
634642
}, [])
635643

644+
const discoveryEnabled = onboardingComplete && localNetworkPermissionState !== "denied" && dropdownMode === "server"
645+
636646
const {
637647
servers,
638648
serversRef,
@@ -655,7 +665,7 @@ export default function DictationScreen() {
655665

656666
const { discoveredServers, discoveryStatus, discoveryError, discoveryAvailable, refreshDiscovery } = useMdnsDiscovery(
657667
{
658-
enabled: onboardingComplete && localNetworkPermissionState !== "denied",
668+
enabled: discoveryEnabled,
659669
},
660670
)
661671

@@ -2000,17 +2010,29 @@ export default function DictationScreen() {
20002010
],
20012011
}))
20022012

2003-
const serverMenuRows = 2 + Math.max(servers.length, 1) + Math.max(discoveredServerOptions.length, 1)
2004-
const menuRows = effectiveDropdownMode === "server" ? serverMenuRows : Math.max(activeServer?.sessions.length ?? 0, 1)
2005-
const expandedRowsHeight = Math.min(menuRows, DROPDOWN_VISIBLE_ROWS) * 42
2013+
const maxDropdownListHeight = DROPDOWN_VISIBLE_ROWS * DROPDOWN_ROW_HEIGHT
2014+
const serverMenuEntries = Math.max(servers.length, 1) + Math.max(discoveredServerOptions.length, 1)
2015+
const estimatedServerMenuRowsHeight = Math.min(
2016+
SERVER_MENU_SECTION_HEIGHT + serverMenuEntries * SERVER_MENU_ENTRY_HEIGHT,
2017+
maxDropdownListHeight,
2018+
)
2019+
const sessionMenuRows = Math.max(activeServer?.sessions.length ?? 0, 1)
2020+
const estimatedSessionMenuRowsHeight = Math.min(sessionMenuRows, DROPDOWN_VISIBLE_ROWS) * DROPDOWN_ROW_HEIGHT
2021+
const serverMenuRowsHeight = Math.min(serverMenuListHeight || estimatedServerMenuRowsHeight, maxDropdownListHeight)
2022+
const sessionMenuRowsHeight = Math.min(sessionMenuListHeight || estimatedSessionMenuRowsHeight, maxDropdownListHeight)
2023+
const expandedRowsHeight = effectiveDropdownMode === "server" ? serverMenuRowsHeight : sessionMenuRowsHeight
2024+
2025+
const estimatedSessionFooterHeight = sessionCreationChoiceCount === 2 ? 72 : sessionCreationChoiceCount === 1 ? 38 : 8
2026+
2027+
const measuredServerFooterHeight = serverMenuFooterHeight || SERVER_MENU_FOOTER_HEIGHT
2028+
const measuredSessionFooterHeight = sessionMenuFooterHeight || estimatedSessionFooterHeight
2029+
20062030
const dropdownFooterExtraHeight =
20072031
effectiveDropdownMode === "server"
2008-
? 38
2009-
: sessionCreationChoiceCount === 2
2010-
? 72
2011-
: sessionCreationChoiceCount === 1
2012-
? 38
2013-
: 8
2032+
? measuredServerFooterHeight
2033+
: showSessionCreationChoices
2034+
? measuredSessionFooterHeight
2035+
: 8
20142036
const expandedHeaderHeight = 51 + 12 + expandedRowsHeight + dropdownFooterExtraHeight
20152037

20162038
const animatedHeaderStyle = useAnimatedStyle(() => ({
@@ -2104,6 +2126,26 @@ export default function DictationScreen() {
21042126
setWaveformLevels(next)
21052127
}, [])
21062128

2129+
const handleServerMenuListLayout = useCallback((event: LayoutChangeEvent) => {
2130+
const next = Math.ceil(event.nativeEvent.layout.height)
2131+
setServerMenuListHeight((prev) => (prev === next ? prev : next))
2132+
}, [])
2133+
2134+
const handleSessionMenuListLayout = useCallback((event: LayoutChangeEvent) => {
2135+
const next = Math.ceil(event.nativeEvent.layout.height)
2136+
setSessionMenuListHeight((prev) => (prev === next ? prev : next))
2137+
}, [])
2138+
2139+
const handleServerMenuFooterLayout = useCallback((event: LayoutChangeEvent) => {
2140+
const next = Math.ceil(event.nativeEvent.layout.height)
2141+
setServerMenuFooterHeight((prev) => (prev === next ? prev : next))
2142+
}, [])
2143+
2144+
const handleSessionMenuFooterLayout = useCallback((event: LayoutChangeEvent) => {
2145+
const next = Math.ceil(event.nativeEvent.layout.height)
2146+
setSessionMenuFooterHeight((prev) => (prev === next ? prev : next))
2147+
}, [])
2148+
21072149
const toggleServerMenu = useCallback(() => {
21082150
void Haptics.selectionAsync().catch(() => {})
21092151
setDropdownMode((prev) => {
@@ -2764,7 +2806,7 @@ export default function DictationScreen() {
27642806
bounces={false}
27652807
>
27662808
{effectiveDropdownMode === "server" ? (
2767-
<>
2809+
<View onLayout={handleServerMenuListLayout}>
27682810
<Text style={styles.serverGroupLabel}>Saved:</Text>
27692811

27702812
{servers.length === 0 ? (
@@ -2833,9 +2875,9 @@ export default function DictationScreen() {
28332875
{discoveryError}
28342876
</Text>
28352877
) : null}
2836-
</>
2878+
</View>
28372879
) : activeServer ? (
2838-
<>
2880+
<View onLayout={handleSessionMenuListLayout}>
28392881
{activeSession ? (
28402882
<>
28412883
<View style={styles.currentSessionSummary}>
@@ -2890,18 +2932,20 @@ export default function DictationScreen() {
28902932
</Pressable>
28912933
))
28922934
)}
2893-
</>
2935+
</View>
28942936
) : (
28952937
<Text style={styles.serverEmptyText}>Select a server first</Text>
28962938
)}
28972939
</ScrollView>
28982940

28992941
{effectiveDropdownMode === "server" ? (
2900-
<Pressable onPress={() => void handleStartScan()} style={styles.addServerButton}>
2901-
<Text style={styles.addServerButtonText}>Add server by scanning QR code</Text>
2902-
</Pressable>
2942+
<View onLayout={handleServerMenuFooterLayout}>
2943+
<Pressable onPress={() => void handleStartScan()} style={styles.addServerButton}>
2944+
<Text style={styles.addServerButtonText}>Add server by scanning QR code</Text>
2945+
</Pressable>
2946+
</View>
29032947
) : effectiveDropdownMode === "session" && activeServer?.status === "online" ? (
2904-
<View style={styles.sessionMenuActions}>
2948+
<View style={styles.sessionMenuActions} onLayout={handleSessionMenuFooterLayout}>
29052949
{activeSession ? (
29062950
<Pressable
29072951
onPress={handleCreateSessionLikeCurrent}
@@ -3887,14 +3931,14 @@ const styles = StyleSheet.create({
38873931
},
38883932
serverMenuInline: {
38893933
marginTop: 8,
3890-
paddingBottom: 8,
3934+
paddingBottom: 2,
38913935
gap: 4,
38923936
},
38933937
dropdownListViewport: {
3894-
maxHeight: DROPDOWN_VISIBLE_ROWS * 42,
3938+
maxHeight: DROPDOWN_VISIBLE_ROWS * DROPDOWN_ROW_HEIGHT,
38953939
},
38963940
dropdownListContent: {
3897-
paddingBottom: 2,
3941+
paddingBottom: 0,
38983942
},
38993943
currentSessionSummary: {
39003944
paddingHorizontal: 4,
@@ -4026,10 +4070,11 @@ const styles = StyleSheet.create({
40264070
fontWeight: "700",
40274071
},
40284072
addServerButton: {
4029-
marginTop: 10,
4073+
marginTop: 4,
40304074
alignSelf: "center",
40314075
paddingHorizontal: 8,
4032-
paddingVertical: 6,
4076+
paddingTop: 2,
4077+
paddingBottom: 10,
40334078
},
40344079
addServerButtonText: {
40354080
color: "#B8BDC9",

0 commit comments

Comments
 (0)