@pylonsync/create-pylon 0.3.54 → 0.3.55
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/package.json +1 -1
- package/templates/expo/chat/apps/expo/App.tsx +126 -156
- package/templates/expo/consumer/apps/expo/App.tsx +211 -179
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pylonsync/create-pylon",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.55",
|
|
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"
|
|
@@ -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
|
});
|