Skip to content

Commit c276a07

Browse files
author
Test
committed
fix: simplify mobile composer autosize
Change-Id: I7d080ee0feec5a84d22f1a6144fca53c5bf1308f Signed-off-by: Test <test@example.com>
1 parent b4f2cfb commit c276a07

24 files changed

+505
-159
lines changed

apps/mobile/src/api/client.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ function parseWorkspaceActivity(value: unknown): WorkspaceActivitySnapshot | nul
9191
if (!isJsonRecord(value)) {
9292
return null;
9393
}
94-
const recency = typeof value.recency === "number" && Number.isFinite(value.recency) ? value.recency : null;
94+
const recency =
95+
typeof value.recency === "number" && Number.isFinite(value.recency) ? value.recency : null;
9596
if (recency === null) {
9697
return null;
9798
}
@@ -286,7 +287,10 @@ export function createClient(cfg: CmuxMobileClientConfig = {}) {
286287
> => {
287288
try {
288289
assert(typeof newName === "string" && newName.trim().length > 0, "newName required");
289-
return await invoke(IPC_CHANNELS.WORKSPACE_FORK, [ensureWorkspaceId(workspaceId), newName.trim()]);
290+
return await invoke(IPC_CHANNELS.WORKSPACE_FORK, [
291+
ensureWorkspaceId(workspaceId),
292+
newName.trim(),
293+
]);
290294
} catch (error) {
291295
return {
292296
success: false,
@@ -324,7 +328,10 @@ export function createClient(cfg: CmuxMobileClientConfig = {}) {
324328
percentage = 1.0
325329
): Promise<Result<void, string>> => {
326330
try {
327-
assert(typeof percentage === "number" && Number.isFinite(percentage), "percentage must be a number");
331+
assert(
332+
typeof percentage === "number" && Number.isFinite(percentage),
333+
"percentage must be a number"
334+
);
328335
await invoke(IPC_CHANNELS.WORKSPACE_TRUNCATE_HISTORY, [
329336
ensureWorkspaceId(workspaceId),
330337
percentage,
@@ -552,7 +559,9 @@ export function createClient(cfg: CmuxMobileClientConfig = {}) {
552559
),
553560
activity: {
554561
list: async (): Promise<Record<string, WorkspaceActivitySnapshot>> => {
555-
const response = await invoke<Record<string, unknown>>(IPC_CHANNELS.WORKSPACE_ACTIVITY_LIST);
562+
const response = await invoke<Record<string, unknown>>(
563+
IPC_CHANNELS.WORKSPACE_ACTIVITY_LIST
564+
);
556565
const result: Record<string, WorkspaceActivitySnapshot> = {};
557566
if (response && typeof response === "object") {
558567
for (const [workspaceId, value] of Object.entries(response)) {
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import React, { useEffect, useRef } from "react";
2+
import {
3+
KeyboardAvoidingView,
4+
Modal,
5+
Platform,
6+
ScrollView,
7+
TextInput,
8+
TouchableOpacity,
9+
View,
10+
} from "react-native";
11+
import { Ionicons } from "@expo/vector-icons";
12+
import { useSafeAreaInsets } from "react-native-safe-area-context";
13+
import { useTheme } from "../theme";
14+
import { ThemedText } from "./ThemedText";
15+
16+
type FullscreenComposerModalProps = {
17+
visible: boolean;
18+
value: string;
19+
placeholder: string;
20+
isEditing: boolean;
21+
isSending: boolean;
22+
onChangeText: (text: string) => void;
23+
onClose: () => void;
24+
onSend: () => Promise<boolean> | boolean;
25+
};
26+
27+
export function FullscreenComposerModal(props: FullscreenComposerModalProps) {
28+
const { visible, value, placeholder, isEditing, isSending, onChangeText, onClose, onSend } =
29+
props;
30+
const theme = useTheme();
31+
const spacing = theme.spacing;
32+
const insets = useSafeAreaInsets();
33+
const inputRef = useRef<TextInput>(null);
34+
35+
useEffect(() => {
36+
if (visible) {
37+
const timeout = setTimeout(() => {
38+
inputRef.current?.focus();
39+
}, 150);
40+
return () => clearTimeout(timeout);
41+
}
42+
return undefined;
43+
}, [visible]);
44+
45+
const disabled = isSending || value.trim().length === 0;
46+
47+
return (
48+
<Modal
49+
animationType="slide"
50+
presentationStyle="fullScreen"
51+
visible={visible}
52+
onRequestClose={onClose}
53+
statusBarTranslucent
54+
>
55+
<KeyboardAvoidingView
56+
style={{ flex: 1 }}
57+
behavior={Platform.OS === "ios" ? "padding" : undefined}
58+
keyboardVerticalOffset={Platform.OS === "ios" ? 0 : 24}
59+
>
60+
<View
61+
style={{
62+
flex: 1,
63+
backgroundColor: theme.colors.surface,
64+
paddingTop: Math.max(insets.top, spacing.lg),
65+
paddingBottom: Math.max(insets.bottom, spacing.lg),
66+
}}
67+
>
68+
<View
69+
style={{
70+
flexDirection: "row",
71+
alignItems: "center",
72+
justifyContent: "space-between",
73+
paddingHorizontal: spacing.lg,
74+
paddingBottom: spacing.md,
75+
borderBottomWidth: 1,
76+
borderBottomColor: theme.colors.border,
77+
}}
78+
>
79+
<TouchableOpacity
80+
onPress={onClose}
81+
accessibilityRole="button"
82+
accessibilityLabel="Close full composer"
83+
style={{
84+
padding: spacing.sm,
85+
borderRadius: spacing.sm,
86+
backgroundColor: theme.colors.surfaceSecondary,
87+
}}
88+
>
89+
<Ionicons name="close" size={20} color={theme.colors.foregroundPrimary} />
90+
</TouchableOpacity>
91+
92+
<ThemedText weight="semibold" style={{ color: theme.colors.foregroundPrimary }}>
93+
{isEditing ? "Edit message" : "Full composer"}
94+
</ThemedText>
95+
96+
<TouchableOpacity
97+
onPress={() => {
98+
const result = onSend();
99+
if (result && typeof (result as Promise<boolean>).then === "function") {
100+
void (result as Promise<boolean>);
101+
}
102+
}}
103+
disabled={disabled}
104+
accessibilityRole="button"
105+
accessibilityLabel="Send message"
106+
style={{
107+
paddingVertical: spacing.sm,
108+
paddingHorizontal: spacing.lg,
109+
borderRadius: theme.radii.sm,
110+
backgroundColor: disabled ? theme.colors.border : theme.colors.accent,
111+
opacity: disabled ? 0.6 : 1,
112+
}}
113+
>
114+
<ThemedText
115+
weight="semibold"
116+
style={{ color: disabled ? theme.colors.foregroundMuted : "#fff" }}
117+
>
118+
{isSending ? "Sending…" : isEditing ? "Save" : "Send"}
119+
</ThemedText>
120+
</TouchableOpacity>
121+
</View>
122+
123+
{isEditing && (
124+
<View
125+
style={{
126+
paddingHorizontal: spacing.lg,
127+
paddingVertical: spacing.sm,
128+
backgroundColor: theme.colors.accentMuted,
129+
borderBottomWidth: 1,
130+
borderBottomColor: theme.colors.border,
131+
}}
132+
>
133+
<ThemedText style={{ color: theme.colors.accent }}>
134+
Editing existing message
135+
</ThemedText>
136+
</View>
137+
)}
138+
139+
<ScrollView
140+
contentContainerStyle={{ flexGrow: 1, padding: spacing.lg }}
141+
keyboardShouldPersistTaps="handled"
142+
>
143+
<View
144+
style={{
145+
flex: 1,
146+
borderWidth: 1,
147+
borderColor: theme.colors.inputBorder,
148+
borderRadius: theme.radii.md,
149+
backgroundColor: theme.colors.inputBackground,
150+
padding: spacing.md,
151+
minHeight: 200,
152+
}}
153+
>
154+
<TextInput
155+
ref={inputRef}
156+
value={value}
157+
onChangeText={onChangeText}
158+
placeholder={placeholder}
159+
placeholderTextColor={theme.colors.foregroundMuted}
160+
style={{
161+
color: theme.colors.foregroundPrimary,
162+
fontSize: 16,
163+
flex: 1,
164+
textAlignVertical: "top",
165+
}}
166+
multiline
167+
autoCorrect={false}
168+
autoCapitalize="sentences"
169+
autoFocus={visible}
170+
/>
171+
</View>
172+
173+
<ThemedText
174+
variant="caption"
175+
style={{
176+
marginTop: spacing.md,
177+
color: theme.colors.foregroundMuted,
178+
textAlign: "center",
179+
}}
180+
>
181+
Draft comfortably here, then tap Send when you are ready.
182+
</ThemedText>
183+
</ScrollView>
184+
</View>
185+
</KeyboardAvoidingView>
186+
</Modal>
187+
);
188+
}

apps/mobile/src/components/MarkdownMessageBody.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import type { JSX } from "react";
22
import { useMemo } from "react";
33
import Markdown from "react-native-markdown-display";
4-
import type { MarkdownStyle } from "react-native-markdown-display";
54
import { useTheme } from "../theme";
65
import { assert } from "../utils/assert";
7-
import { createMarkdownStyles, type MarkdownVariant } from "../messages/markdownStyles";
6+
import {
7+
createMarkdownStyles,
8+
type MarkdownVariant,
9+
type MarkdownStyle,
10+
} from "../messages/markdownStyles";
811
import { normalizeMarkdown } from "../messages/markdownUtils";
912

1013
export interface MarkdownMessageBodyProps {
@@ -13,10 +16,14 @@ export interface MarkdownMessageBodyProps {
1316
styleOverrides?: Partial<MarkdownStyle>;
1417
}
1518

16-
export function MarkdownMessageBody({ content, variant, styleOverrides }: MarkdownMessageBodyProps): JSX.Element | null {
19+
export function MarkdownMessageBody({
20+
content,
21+
variant,
22+
styleOverrides,
23+
}: MarkdownMessageBodyProps): JSX.Element | null {
1724
assert(
1825
content === undefined || content === null || typeof content === "string",
19-
"MarkdownMessageBody expects string content",
26+
"MarkdownMessageBody expects string content"
2027
);
2128

2229
const theme = useTheme();
@@ -43,7 +50,7 @@ export function MarkdownMessageBody({ content, variant, styleOverrides }: Markdo
4350
return {
4451
...base,
4552
...styleOverrides,
46-
} satisfies MarkdownStyle;
53+
} as MarkdownStyle;
4754
}, [theme, variant, styleOverrides]);
4855

4956
return <Markdown style={markdownStyles}>{normalizedContent}</Markdown>;

0 commit comments

Comments
 (0)