feat: open the composer as a centred panel on tablets

This commit is contained in:
Linus Rath
2026-10-15 02:08:18 +02:00
parent 2470e99dc8
commit 0087e4c8ff
2 changed files with 40 additions and 8 deletions
+6 -4
View File
@@ -394,6 +394,7 @@ export default function App() {
// cached mail list instead of the "Restoring session" spinner; the real // cached mail list instead of the "Restoring session" spinner; the real
// JMAP session comes up in the background. // JMAP session comes up in the background.
const hasPersistedAccount = useAccountStore((state) => state.activeAccountId != null); const hasPersistedAccount = useAccountStore((state) => state.activeAccountId != null);
const { isSplit } = useLayout();
React.useEffect(() => { React.useEffect(() => {
if (!hasRestoredSession) { if (!hasRestoredSession) {
@@ -824,10 +825,11 @@ export default function App() {
<Stack.Screen <Stack.Screen
name="Compose" name="Compose"
component={ComposeScreen} component={ComposeScreen}
options={{ // A tablet shows the composer as a panel over the screen it came
presentation: 'modal', // from, so that screen stays drawn behind the dimmed backdrop.
animation: 'slide_from_bottom', options={isSplit
}} ? { presentation: 'transparentModal', animation: 'fade' }
: { presentation: 'modal', animation: 'slide_from_bottom' }}
/> />
<Stack.Screen name="ContactDetail" component={ContactDetailScreen} /> <Stack.Screen name="ContactDetail" component={ContactDetailScreen} />
<Stack.Screen <Stack.Screen
+34 -4
View File
@@ -399,11 +399,15 @@ export default function ComposeScreen({ route, navigation }: Props) {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
// Track the visible keyboard obstruction so the format bar stays above it. // Track the visible keyboard obstruction so the format bar stays above it.
const kbObstruction = useKeyboardObstruction(insets.bottom, () => editorRef.current?.blur()); const kbObstruction = useKeyboardObstruction(insets.bottom, () => editorRef.current?.blur());
const { shortHeight } = useLayout(); const { shortHeight, isSplit } = useLayout();
const typingInShortWindow = shortHeight && kbObstruction > 0; const typingInShortWindow = shortHeight && kbObstruction > 0;
// When the keyboard is up it covers the bottom safe area, so we don't // When the keyboard is up it covers the bottom safe area, so we don't
// need to add it on top — pad by whichever is larger. // need to add it on top — pad by whichever is larger.
const bottomPad = Math.max(kbObstruction, insets.bottom); // On a tablet the composer is a centred panel that stops PANEL_MARGIN
// short of the screen bottom; the keyboard covers that much less of it.
const bottomPad = isSplit
? Math.max(0, kbObstruction - PANEL_MARGIN)
: Math.max(kbObstruction, insets.bottom);
const replyTo = route.params?.replyTo; const replyTo = route.params?.replyTo;
const draft = route.params?.draft; const draft = route.params?.draft;
const mode = route.params?.mode ?? 'compose'; const mode = route.params?.mode ?? 'compose';
@@ -2263,8 +2267,8 @@ export default function ComposeScreen({ route, navigation }: Props) {
); );
}; };
return ( const composer = (
<SafeAreaView style={styles.container} edges={['top', 'left', 'right']}> <SafeAreaView style={styles.container} edges={isSplit ? [] : ['top', 'left', 'right']}>
<View style={[styles.header, typingInShortWindow && styles.headerCompact]}> <View style={[styles.header, typingInShortWindow && styles.headerCompact]}>
<Pressable <Pressable
onPress={onClose} onPress={onClose}
@@ -2851,12 +2855,38 @@ export default function ComposeScreen({ route, navigation }: Props) {
/> />
</SafeAreaView> </SafeAreaView>
); );
if (!isSplit) return composer;
// Tablet: a centred panel over the dimmed screen it was opened from.
return (
<View style={[styles.panelBackdrop, { paddingTop: insets.top + PANEL_MARGIN, paddingBottom: insets.bottom + PANEL_MARGIN }]}>
<View style={styles.panel}>{composer}</View>
</View>
);
} }
/** Gap between the tablet composer panel and the screen edges. */
const PANEL_MARGIN = 24;
function makeStyles(c: ThemePalette) { function makeStyles(c: ThemePalette) {
return StyleSheet.create({ return StyleSheet.create({
container: { flex: 1, backgroundColor: c.background }, container: { flex: 1, backgroundColor: c.background },
flex: { flex: 1 }, flex: { flex: 1 },
panelBackdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
alignItems: 'center',
paddingHorizontal: PANEL_MARGIN,
},
panel: {
flex: 1,
width: '100%',
maxWidth: 760,
borderRadius: radius.lg,
borderWidth: 1,
borderColor: c.border,
overflow: 'hidden',
backgroundColor: c.background,
},
scrollContent: { flexGrow: 1 }, scrollContent: { flexGrow: 1 },
header: { header: {