@pylonsync/create-pylon 0.3.54 → 0.3.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-pylon.js +28 -51
- package/package.json +1 -1
- package/templates/_root/turbo.json +18 -0
- package/templates/backend/b2b/apps/api/package.json +3 -3
- package/templates/backend/barebones/apps/api/package.json +3 -3
- package/templates/backend/chat/apps/api/package.json +3 -3
- package/templates/backend/consumer/apps/api/package.json +3 -3
- package/templates/backend/todo/apps/api/package.json +3 -3
- package/templates/expo/barebones/apps/expo/package.json +1 -0
- package/templates/expo/chat/apps/expo/App.tsx +126 -156
- package/templates/expo/chat/apps/expo/package.json +1 -0
- package/templates/expo/consumer/apps/expo/App.tsx +211 -179
- package/templates/expo/consumer/apps/expo/package.json +1 -0
- package/templates/expo/todo/apps/expo/package.json +1 -0
- package/templates/ios/chat/apps/ios/Package.swift +1 -11
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +26 -30
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/RoomView.swift +35 -36
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +27 -4
- package/templates/ios/chat/apps/ios/project.yml +2 -0
- package/templates/ios/consumer/apps/ios/Package.swift +1 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +81 -52
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +6 -14
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +14 -6
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -15
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +33 -5
- package/templates/ios/consumer/apps/ios/project.yml +2 -0
- package/templates/mac/chat/apps/mac/Package.swift +1 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +30 -27
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/RoomView.swift +35 -36
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +26 -5
- package/templates/mac/chat/apps/mac/project.yml +2 -0
- package/templates/mac/consumer/apps/mac/Package.swift +1 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +81 -52
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +6 -14
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +14 -6
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -15
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +34 -6
- package/templates/mac/consumer/apps/mac/project.yml +2 -0
- package/templates/web/chat/apps/web/src/app/components/ChatRoom.tsx +52 -71
- package/templates/web/chat/apps/web/src/app/layout.tsx +3 -2
- package/templates/web/chat/apps/web/src/app/page.tsx +5 -44
- package/templates/web/chat/apps/web/src/app/providers.tsx +25 -0
- package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +139 -119
- package/templates/web/consumer/apps/web/src/app/layout.tsx +3 -2
- package/templates/web/consumer/apps/web/src/app/page.tsx +8 -42
- package/templates/web/consumer/apps/web/src/app/providers.tsx +25 -0
package/bin/create-pylon.js
CHANGED
|
@@ -340,28 +340,37 @@ walkAndSubstitute(root);
|
|
|
340
340
|
// picked (each PM exposes "run X in workspace Y" differently).
|
|
341
341
|
// ---------------------------------------------------------------------------
|
|
342
342
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
343
|
+
// Turborepo orchestrates the workspace. `turbo dev` runs the `dev`
|
|
344
|
+
// task in every package that defines one (apps/api always; apps/web,
|
|
345
|
+
// apps/expo when scaffolded). Native targets (ios, mac) aren't
|
|
346
|
+
// `turbo dev`-shaped — Xcode / `swift run` block — so they get
|
|
347
|
+
// dedicated escape-hatch scripts instead. turbo.json ships in
|
|
348
|
+
// _root/, so it's already in the project.
|
|
349
|
+
const helperScripts = {};
|
|
349
350
|
if (platforms.includes("ios")) {
|
|
350
|
-
|
|
351
|
-
// then it's an Xcode-driven flow — no `bun run dev` semantics.
|
|
352
|
-
devScripts["dev:ios"] =
|
|
351
|
+
helperScripts["dev:ios"] =
|
|
353
352
|
"echo 'cd apps/ios && xcodegen generate && open *.xcodeproj (or: swift run for a quick macOS preview)'";
|
|
354
353
|
}
|
|
355
354
|
if (platforms.includes("mac")) {
|
|
356
|
-
|
|
355
|
+
helperScripts["dev:mac"] =
|
|
357
356
|
"echo 'cd apps/mac && swift run (or: xcodegen generate && open *.xcodeproj)'";
|
|
358
357
|
}
|
|
359
358
|
|
|
360
|
-
|
|
359
|
+
// Turbo 2.x refuses to run without packageManager set. Pick a recent-
|
|
360
|
+
// stable for whichever PM the user picked. npm doesn't enforce this
|
|
361
|
+
// field but turbo still expects it to be present.
|
|
362
|
+
const PACKAGE_MANAGERS = {
|
|
363
|
+
bun: "bun@1.2.19",
|
|
364
|
+
pnpm: "pnpm@9.12.0",
|
|
365
|
+
yarn: "yarn@4.5.0",
|
|
366
|
+
npm: "npm@10.9.0",
|
|
367
|
+
};
|
|
368
|
+
|
|
361
369
|
const rootPkg = {
|
|
362
370
|
name: APP_NAME_KEBAB,
|
|
363
371
|
private: true,
|
|
364
372
|
type: "module",
|
|
373
|
+
packageManager: PACKAGE_MANAGERS[flags.pm],
|
|
365
374
|
workspaces: ["apps/*", "packages/*"].filter((p) => {
|
|
366
375
|
// Only declare packages/* as a workspace if we actually scaffolded
|
|
367
376
|
// packages/ui — otherwise the empty match warns on bun install.
|
|
@@ -369,14 +378,15 @@ const rootPkg = {
|
|
|
369
378
|
return true;
|
|
370
379
|
}),
|
|
371
380
|
scripts: {
|
|
372
|
-
dev:
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
...
|
|
377
|
-
|
|
381
|
+
dev: "turbo dev",
|
|
382
|
+
build: "turbo build",
|
|
383
|
+
check: "turbo check",
|
|
384
|
+
lint: "turbo lint",
|
|
385
|
+
...helperScripts,
|
|
386
|
+
},
|
|
387
|
+
devDependencies: {
|
|
388
|
+
turbo: "^2.3.0",
|
|
378
389
|
},
|
|
379
|
-
devDependencies: parallelDevs.length > 1 ? { "npm-run-all": "^4.1.5" } : {},
|
|
380
390
|
};
|
|
381
391
|
writeFileSync(
|
|
382
392
|
join(root, "package.json"),
|
|
@@ -457,36 +467,3 @@ function detectPackageManager() {
|
|
|
457
467
|
return null;
|
|
458
468
|
}
|
|
459
469
|
|
|
460
|
-
function pmScripts(pm) {
|
|
461
|
-
switch (pm) {
|
|
462
|
-
case "bun":
|
|
463
|
-
return {
|
|
464
|
-
devApi: "bun run --filter './apps/api' dev",
|
|
465
|
-
devWeb: "bun run --filter './apps/web' dev",
|
|
466
|
-
devExpo: "bun run --filter './apps/expo' start",
|
|
467
|
-
build: "bun run --filter '*' build",
|
|
468
|
-
};
|
|
469
|
-
case "pnpm":
|
|
470
|
-
return {
|
|
471
|
-
devApi: "pnpm --filter './apps/api' run dev",
|
|
472
|
-
devWeb: "pnpm --filter './apps/web' run dev",
|
|
473
|
-
devExpo: "pnpm --filter './apps/expo' run start",
|
|
474
|
-
build: "pnpm --filter '*' run build",
|
|
475
|
-
};
|
|
476
|
-
case "yarn":
|
|
477
|
-
return {
|
|
478
|
-
devApi: `yarn workspace @${APP_NAME_KEBAB}/api run dev`,
|
|
479
|
-
devWeb: `yarn workspace @${APP_NAME_KEBAB}/web run dev`,
|
|
480
|
-
devExpo: `yarn workspace @${APP_NAME_KEBAB}/expo run start`,
|
|
481
|
-
build: "yarn workspaces foreach -A run build",
|
|
482
|
-
};
|
|
483
|
-
case "npm":
|
|
484
|
-
default:
|
|
485
|
-
return {
|
|
486
|
-
devApi: "npm --workspace apps/api run dev",
|
|
487
|
-
devWeb: "npm --workspace apps/web run dev",
|
|
488
|
-
devExpo: "npm --workspace apps/expo run start",
|
|
489
|
-
build: "npm --workspaces run build --if-present",
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pylonsync/create-pylon",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.57",
|
|
4
4
|
"description": "Scaffold a new Pylon app — realtime backend + web/mobile/expo frontends in one command. Run via `npm create @pylonsync/pylon@latest`.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://turbo.build/schema.json",
|
|
3
|
+
"ui": "stream",
|
|
4
|
+
"tasks": {
|
|
5
|
+
"dev": {
|
|
6
|
+
"cache": false,
|
|
7
|
+
"persistent": true
|
|
8
|
+
},
|
|
9
|
+
"build": {
|
|
10
|
+
"dependsOn": ["^build"],
|
|
11
|
+
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
|
|
12
|
+
},
|
|
13
|
+
"check": {
|
|
14
|
+
"dependsOn": ["^build"]
|
|
15
|
+
},
|
|
16
|
+
"lint": {}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "pylon dev
|
|
8
|
-
"build": "pylon
|
|
9
|
-
"schema:push": "pylon schema push
|
|
7
|
+
"dev": "pylon dev",
|
|
8
|
+
"build": "pylon build",
|
|
9
|
+
"schema:push": "pylon schema push --sqlite dev.db",
|
|
10
10
|
"schema:inspect": "pylon schema inspect --sqlite dev.db"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "pylon dev
|
|
8
|
-
"build": "pylon
|
|
9
|
-
"schema:push": "pylon schema push
|
|
7
|
+
"dev": "pylon dev",
|
|
8
|
+
"build": "pylon build",
|
|
9
|
+
"schema:push": "pylon schema push --sqlite dev.db",
|
|
10
10
|
"schema:inspect": "pylon schema inspect --sqlite dev.db"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "pylon dev
|
|
8
|
-
"build": "pylon
|
|
9
|
-
"schema:push": "pylon schema push
|
|
7
|
+
"dev": "pylon dev",
|
|
8
|
+
"build": "pylon build",
|
|
9
|
+
"schema:push": "pylon schema push --sqlite dev.db",
|
|
10
10
|
"schema:inspect": "pylon schema inspect --sqlite dev.db"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "pylon dev
|
|
8
|
-
"build": "pylon
|
|
9
|
-
"schema:push": "pylon schema push
|
|
7
|
+
"dev": "pylon dev",
|
|
8
|
+
"build": "pylon build",
|
|
9
|
+
"schema:push": "pylon schema push --sqlite dev.db",
|
|
10
10
|
"schema:inspect": "pylon schema inspect --sqlite dev.db"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "pylon dev
|
|
8
|
-
"build": "pylon
|
|
9
|
-
"schema:push": "pylon schema push
|
|
7
|
+
"dev": "pylon dev",
|
|
8
|
+
"build": "pylon build",
|
|
9
|
+
"schema:push": "pylon schema push --sqlite dev.db",
|
|
10
10
|
"schema:inspect": "pylon schema inspect --sqlite dev.db"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
SafeAreaView,
|
|
14
14
|
} from "react-native";
|
|
15
15
|
import { StatusBar } from "expo-status-bar";
|
|
16
|
-
import { init, callFn } from "@pylonsync/react-native";
|
|
16
|
+
import { init, db, callFn } from "@pylonsync/react-native";
|
|
17
17
|
|
|
18
18
|
const PYLON_BASE_URL =
|
|
19
19
|
process.env.EXPO_PUBLIC_PYLON_BASE_URL ??
|
|
@@ -48,23 +48,9 @@ function ensureInit() {
|
|
|
48
48
|
|
|
49
49
|
export default function App() {
|
|
50
50
|
const [ready, setReady] = useState(false);
|
|
51
|
-
const [rooms, setRooms] = useState<Room[]>([]);
|
|
52
|
-
const [active, setActive] = useState<Room | null>(null);
|
|
53
|
-
const [authorName, setAuthorName] = useState("anonymous");
|
|
54
|
-
|
|
55
51
|
useEffect(() => {
|
|
56
|
-
ensureInit().then(
|
|
57
|
-
try {
|
|
58
|
-
const list = await callFn<Room[]>("listRooms", {});
|
|
59
|
-
setRooms(list);
|
|
60
|
-
setActive(list[0] ?? null);
|
|
61
|
-
} catch (e) {
|
|
62
|
-
Alert.alert("Load failed", String(e));
|
|
63
|
-
}
|
|
64
|
-
setReady(true);
|
|
65
|
-
});
|
|
52
|
+
ensureInit().then(() => setReady(true));
|
|
66
53
|
}, []);
|
|
67
|
-
|
|
68
54
|
if (!ready) {
|
|
69
55
|
return (
|
|
70
56
|
<View style={[styles.screen, styles.center]}>
|
|
@@ -72,159 +58,61 @@ export default function App() {
|
|
|
72
58
|
</View>
|
|
73
59
|
);
|
|
74
60
|
}
|
|
75
|
-
|
|
76
|
-
if (!active) {
|
|
77
|
-
return (
|
|
78
|
-
<RoomCreate
|
|
79
|
-
authorName={authorName}
|
|
80
|
-
onAuthorNameChange={setAuthorName}
|
|
81
|
-
onCreated={(r) => {
|
|
82
|
-
setRooms((prev) => [...prev, r]);
|
|
83
|
-
setActive(r);
|
|
84
|
-
}}
|
|
85
|
-
/>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
<RoomView
|
|
91
|
-
room={active}
|
|
92
|
-
rooms={rooms}
|
|
93
|
-
onSwitch={setActive}
|
|
94
|
-
authorName={authorName}
|
|
95
|
-
onAuthorNameChange={setAuthorName}
|
|
96
|
-
onCreate={(r) => {
|
|
97
|
-
setRooms((prev) => [...prev, r]);
|
|
98
|
-
setActive(r);
|
|
99
|
-
}}
|
|
100
|
-
/>
|
|
101
|
-
);
|
|
61
|
+
return <Chat />;
|
|
102
62
|
}
|
|
103
63
|
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const [creating, setCreating] = useState(false);
|
|
64
|
+
function Chat() {
|
|
65
|
+
// Live subscriptions — sync engine pushes diffs over WebSocket.
|
|
66
|
+
// New rooms / new messages from any other device or device update
|
|
67
|
+
// re-render this component without polling.
|
|
68
|
+
const { data: rooms = [] } = db.useQuery<Room>("Room", {
|
|
69
|
+
orderBy: { createdAt: "asc" },
|
|
70
|
+
});
|
|
71
|
+
const [activeId, setActiveId] = useState<string | null>(null);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!activeId && rooms.length > 0) setActiveId(rooms[0].id);
|
|
74
|
+
}, [rooms, activeId]);
|
|
116
75
|
|
|
117
|
-
|
|
118
|
-
setCreating(true);
|
|
119
|
-
try {
|
|
120
|
-
const r = await callFn<Room>("createRoom", {
|
|
121
|
-
slug: slug.toLowerCase(),
|
|
122
|
-
name,
|
|
123
|
-
});
|
|
124
|
-
onCreated(r);
|
|
125
|
-
} catch (e) {
|
|
126
|
-
Alert.alert("Create failed", String(e));
|
|
127
|
-
} finally {
|
|
128
|
-
setCreating(false);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
76
|
+
const active = rooms.find((r) => r.id === activeId) ?? null;
|
|
131
77
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
<Text style={styles.subtitle}>No rooms yet — create one.</Text>
|
|
138
|
-
<TextInput
|
|
139
|
-
style={styles.input}
|
|
140
|
-
placeholder="Your display name"
|
|
141
|
-
value={authorName}
|
|
142
|
-
onChangeText={onAuthorNameChange}
|
|
143
|
-
/>
|
|
144
|
-
<TextInput
|
|
145
|
-
style={styles.input}
|
|
146
|
-
placeholder="Room name"
|
|
147
|
-
value={name}
|
|
148
|
-
onChangeText={setName}
|
|
149
|
-
/>
|
|
150
|
-
<TextInput
|
|
151
|
-
style={styles.input}
|
|
152
|
-
placeholder="Slug"
|
|
153
|
-
value={slug}
|
|
154
|
-
onChangeText={(s) => setSlug(s.toLowerCase())}
|
|
155
|
-
autoCapitalize="none"
|
|
156
|
-
autoCorrect={false}
|
|
157
|
-
/>
|
|
158
|
-
<Pressable
|
|
159
|
-
onPress={create}
|
|
160
|
-
disabled={creating || !name.trim() || !slug.trim()}
|
|
161
|
-
style={({ pressed }) => [
|
|
162
|
-
styles.button,
|
|
163
|
-
(creating || !name.trim() || !slug.trim()) && styles.buttonDisabled,
|
|
164
|
-
pressed && styles.buttonPressed,
|
|
165
|
-
]}
|
|
166
|
-
>
|
|
167
|
-
<Text style={styles.buttonLabel}>
|
|
168
|
-
{creating ? "Creating…" : "Create room"}
|
|
169
|
-
</Text>
|
|
170
|
-
</Pressable>
|
|
171
|
-
</View>
|
|
172
|
-
</SafeAreaView>
|
|
173
|
-
);
|
|
174
|
-
}
|
|
78
|
+
const { data: messages = [] } = db.useQuery<Message>("Message", {
|
|
79
|
+
where: activeId ? { roomId: activeId } : undefined,
|
|
80
|
+
orderBy: { createdAt: "asc" },
|
|
81
|
+
limit: 200,
|
|
82
|
+
});
|
|
175
83
|
|
|
176
|
-
function RoomView({
|
|
177
|
-
room,
|
|
178
|
-
rooms,
|
|
179
|
-
onSwitch,
|
|
180
|
-
authorName,
|
|
181
|
-
onAuthorNameChange,
|
|
182
|
-
onCreate,
|
|
183
|
-
}: {
|
|
184
|
-
room: Room;
|
|
185
|
-
rooms: Room[];
|
|
186
|
-
onSwitch: (r: Room) => void;
|
|
187
|
-
authorName: string;
|
|
188
|
-
onAuthorNameChange: (s: string) => void;
|
|
189
|
-
onCreate: (r: Room) => void;
|
|
190
|
-
}) {
|
|
191
|
-
const [messages, setMessages] = useState<Message[]>([]);
|
|
192
84
|
const [draft, setDraft] = useState("");
|
|
193
85
|
const [sending, setSending] = useState(false);
|
|
86
|
+
const [authorName, setAuthorName] = useState("anonymous");
|
|
194
87
|
const listRef = useRef<FlatList<Message>>(null);
|
|
195
88
|
|
|
196
|
-
useEffect(() => {
|
|
197
|
-
void load();
|
|
198
|
-
const t = setInterval(load, 1500);
|
|
199
|
-
return () => clearInterval(t);
|
|
200
|
-
async function load() {
|
|
201
|
-
try {
|
|
202
|
-
const m = await callFn<Message[]>("roomMessages", { roomId: room.id });
|
|
203
|
-
setMessages(m);
|
|
204
|
-
} catch {
|
|
205
|
-
// ignore — will retry on next tick
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}, [room.id]);
|
|
209
|
-
|
|
210
89
|
useEffect(() => {
|
|
211
90
|
if (messages.length > 0) {
|
|
212
91
|
listRef.current?.scrollToEnd({ animated: true });
|
|
213
92
|
}
|
|
214
|
-
}, [messages.length]);
|
|
93
|
+
}, [messages.length, activeId]);
|
|
94
|
+
|
|
95
|
+
if (!active) {
|
|
96
|
+
return (
|
|
97
|
+
<RoomCreate
|
|
98
|
+
authorName={authorName}
|
|
99
|
+
onAuthorNameChange={setAuthorName}
|
|
100
|
+
onCreated={(r) => setActiveId(r.id)}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
215
104
|
|
|
216
105
|
async function send() {
|
|
217
106
|
const body = draft.trim();
|
|
218
107
|
if (!body) return;
|
|
219
|
-
setSending(true);
|
|
220
108
|
setDraft("");
|
|
109
|
+
setSending(true);
|
|
221
110
|
try {
|
|
222
|
-
|
|
223
|
-
roomId:
|
|
111
|
+
await callFn("sendMessage", {
|
|
112
|
+
roomId: active.id,
|
|
224
113
|
body,
|
|
225
114
|
authorName,
|
|
226
115
|
});
|
|
227
|
-
setMessages((prev) => [...prev, msg]);
|
|
228
116
|
} catch (e) {
|
|
229
117
|
setDraft(body);
|
|
230
118
|
Alert.alert("Send failed", String(e));
|
|
@@ -241,8 +129,8 @@ function RoomView({
|
|
|
241
129
|
.replace(/[^a-z0-9]+/g, "-")
|
|
242
130
|
.replace(/^-|-$/g, "");
|
|
243
131
|
try {
|
|
244
|
-
const r = await callFn
|
|
245
|
-
|
|
132
|
+
const r = (await callFn("createRoom", { slug, name })) as Room;
|
|
133
|
+
setActiveId(r.id);
|
|
246
134
|
} catch (e) {
|
|
247
135
|
Alert.alert("Create failed", String(e));
|
|
248
136
|
}
|
|
@@ -258,15 +146,14 @@ function RoomView({
|
|
|
258
146
|
>
|
|
259
147
|
<View style={styles.header}>
|
|
260
148
|
<View>
|
|
261
|
-
<Text style={styles.title}>{
|
|
262
|
-
<Text style={styles.handle}>#{
|
|
149
|
+
<Text style={styles.title}>{active.name}</Text>
|
|
150
|
+
<Text style={styles.handle}>#{active.slug}</Text>
|
|
263
151
|
</View>
|
|
264
152
|
{rooms.length > 1 && (
|
|
265
153
|
<Pressable
|
|
266
154
|
onPress={() => {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
onSwitch(rooms[(idx + 1) % rooms.length]);
|
|
155
|
+
const idx = rooms.findIndex((r) => r.id === active.id);
|
|
156
|
+
setActiveId(rooms[(idx + 1) % rooms.length].id);
|
|
270
157
|
}}
|
|
271
158
|
>
|
|
272
159
|
<Text style={styles.switchBtn}>Next room →</Text>
|
|
@@ -279,7 +166,7 @@ function RoomView({
|
|
|
279
166
|
<TextInput
|
|
280
167
|
style={styles.nameinput}
|
|
281
168
|
value={authorName}
|
|
282
|
-
onChangeText={
|
|
169
|
+
onChangeText={setAuthorName}
|
|
283
170
|
autoCapitalize="none"
|
|
284
171
|
/>
|
|
285
172
|
</View>
|
|
@@ -313,7 +200,7 @@ function RoomView({
|
|
|
313
200
|
style={styles.composerInput}
|
|
314
201
|
value={draft}
|
|
315
202
|
onChangeText={setDraft}
|
|
316
|
-
placeholder={`Message ${
|
|
203
|
+
placeholder={`Message ${active.name}…`}
|
|
317
204
|
multiline
|
|
318
205
|
editable={!sending}
|
|
319
206
|
/>
|
|
@@ -329,11 +216,87 @@ function RoomView({
|
|
|
329
216
|
<Text style={styles.buttonLabel}>Send</Text>
|
|
330
217
|
</Pressable>
|
|
331
218
|
</View>
|
|
219
|
+
|
|
220
|
+
<Pressable onPress={createRoom} style={styles.newRoomBar}>
|
|
221
|
+
<Text style={styles.newRoomLabel}>+ New room</Text>
|
|
222
|
+
</Pressable>
|
|
332
223
|
</KeyboardAvoidingView>
|
|
333
224
|
</SafeAreaView>
|
|
334
225
|
);
|
|
335
226
|
}
|
|
336
227
|
|
|
228
|
+
function RoomCreate({
|
|
229
|
+
authorName,
|
|
230
|
+
onAuthorNameChange,
|
|
231
|
+
onCreated,
|
|
232
|
+
}: {
|
|
233
|
+
authorName: string;
|
|
234
|
+
onAuthorNameChange: (s: string) => void;
|
|
235
|
+
onCreated: (r: Room) => void;
|
|
236
|
+
}) {
|
|
237
|
+
const [name, setName] = useState("General");
|
|
238
|
+
const [slug, setSlug] = useState("general");
|
|
239
|
+
const [creating, setCreating] = useState(false);
|
|
240
|
+
|
|
241
|
+
async function create() {
|
|
242
|
+
setCreating(true);
|
|
243
|
+
try {
|
|
244
|
+
const r = (await callFn("createRoom", {
|
|
245
|
+
slug: slug.toLowerCase(),
|
|
246
|
+
name,
|
|
247
|
+
})) as Room;
|
|
248
|
+
onCreated(r);
|
|
249
|
+
} catch (e) {
|
|
250
|
+
Alert.alert("Create failed", String(e));
|
|
251
|
+
} finally {
|
|
252
|
+
setCreating(false);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<SafeAreaView style={styles.screen}>
|
|
258
|
+
<StatusBar style="auto" />
|
|
259
|
+
<View style={styles.content}>
|
|
260
|
+
<Text style={styles.title}>__APP_NAME__</Text>
|
|
261
|
+
<Text style={styles.subtitle}>No rooms yet — create one.</Text>
|
|
262
|
+
<TextInput
|
|
263
|
+
style={styles.input}
|
|
264
|
+
placeholder="Your display name"
|
|
265
|
+
value={authorName}
|
|
266
|
+
onChangeText={onAuthorNameChange}
|
|
267
|
+
/>
|
|
268
|
+
<TextInput
|
|
269
|
+
style={styles.input}
|
|
270
|
+
placeholder="Room name"
|
|
271
|
+
value={name}
|
|
272
|
+
onChangeText={setName}
|
|
273
|
+
/>
|
|
274
|
+
<TextInput
|
|
275
|
+
style={styles.input}
|
|
276
|
+
placeholder="Slug"
|
|
277
|
+
value={slug}
|
|
278
|
+
onChangeText={(s) => setSlug(s.toLowerCase())}
|
|
279
|
+
autoCapitalize="none"
|
|
280
|
+
autoCorrect={false}
|
|
281
|
+
/>
|
|
282
|
+
<Pressable
|
|
283
|
+
onPress={create}
|
|
284
|
+
disabled={creating || !name.trim() || !slug.trim()}
|
|
285
|
+
style={({ pressed }) => [
|
|
286
|
+
styles.button,
|
|
287
|
+
(creating || !name.trim() || !slug.trim()) && styles.buttonDisabled,
|
|
288
|
+
pressed && styles.buttonPressed,
|
|
289
|
+
]}
|
|
290
|
+
>
|
|
291
|
+
<Text style={styles.buttonLabel}>
|
|
292
|
+
{creating ? "Creating…" : "Create room"}
|
|
293
|
+
</Text>
|
|
294
|
+
</Pressable>
|
|
295
|
+
</View>
|
|
296
|
+
</SafeAreaView>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
337
300
|
const styles = StyleSheet.create({
|
|
338
301
|
screen: { flex: 1, backgroundColor: "#fff" },
|
|
339
302
|
flex: { flex: 1 },
|
|
@@ -411,4 +374,11 @@ const styles = StyleSheet.create({
|
|
|
411
374
|
paddingVertical: 8,
|
|
412
375
|
fontSize: 14,
|
|
413
376
|
},
|
|
377
|
+
newRoomBar: {
|
|
378
|
+
paddingVertical: 10,
|
|
379
|
+
alignItems: "center",
|
|
380
|
+
borderTopWidth: 1,
|
|
381
|
+
borderColor: "#e5e5e5",
|
|
382
|
+
},
|
|
383
|
+
newRoomLabel: { color: "#3b82f6", fontSize: 13 },
|
|
414
384
|
});
|