@tenyokjs/devdevian-cli 0.5.0-alpha.1 → 0.6.0-alpha.1
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/README.md +27 -2
- package/VERSIONS.md +19 -1
- package/bin/devdevian.mjs +1 -0
- package/lib/appearance.mjs +8 -0
- package/lib/catalog.mjs +611 -0
- package/lib/client.mjs +14 -3
- package/lib/context-actions.mjs +2 -1
- package/lib/devi.mjs +137 -0
- package/lib/drafts.mjs +11 -4
- package/lib/picker.mjs +34 -12
- package/lib/tui-data.mjs +10 -2
- package/lib/tui-model.mjs +3 -1
- package/lib/tui.mjs +480 -81
- package/package.json +1 -1
package/lib/tui.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { render, Box, Text, useApp, useInput, useWindowSize } from "ink";
|
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { homedir, hostname } from "node:os";
|
|
5
5
|
import { workspaceRoot, reviewSource } from "./workspace.mjs";
|
|
6
|
+
import { DeviPanel } from "./devi.mjs";
|
|
6
7
|
import { WorkspacePicker } from "./picker.mjs";
|
|
7
8
|
import { DraftStore, draftIdentity } from "./drafts.mjs";
|
|
8
9
|
import { spawn } from "node:child_process";
|
|
@@ -47,6 +48,18 @@ import { editExternal } from "./external-editor.mjs";
|
|
|
47
48
|
import { markdownRows } from "./rich-text.mjs";
|
|
48
49
|
import { problemMessage } from "./problems.mjs";
|
|
49
50
|
import { ReviewWorkspace, codeRange } from "./review-workspace.mjs";
|
|
51
|
+
import {
|
|
52
|
+
catalogFields,
|
|
53
|
+
initialCatalog,
|
|
54
|
+
catalogDocument,
|
|
55
|
+
prepareCatalog,
|
|
56
|
+
publishCatalog,
|
|
57
|
+
saveCalendar,
|
|
58
|
+
validateFilter,
|
|
59
|
+
localTime,
|
|
60
|
+
viewerZone,
|
|
61
|
+
webURL,
|
|
62
|
+
} from "./catalog.mjs";
|
|
50
63
|
const h = React.createElement;
|
|
51
64
|
const itemsOf = (value) => (Array.isArray(value) ? value : value?.items || []);
|
|
52
65
|
const stamp = (value) => {
|
|
@@ -73,6 +86,8 @@ const labelOf = (route) =>
|
|
|
73
86
|
community: "Community",
|
|
74
87
|
feedback: "Code feedback",
|
|
75
88
|
thread: "Reply thread",
|
|
89
|
+
project: "Project",
|
|
90
|
+
event: "Event",
|
|
76
91
|
}[route.view] ||
|
|
77
92
|
"Workspace";
|
|
78
93
|
function openBrowser(url) {
|
|
@@ -91,6 +106,8 @@ function openBrowser(url) {
|
|
|
91
106
|
child.unref();
|
|
92
107
|
}
|
|
93
108
|
function fieldsFor(form) {
|
|
109
|
+
if (["catalog", "catalogFilter", "calendar"].includes(form.kind))
|
|
110
|
+
return catalogFields(form);
|
|
94
111
|
if (form.kind === "appearance") return appearanceFields;
|
|
95
112
|
if (form.kind === "publication")
|
|
96
113
|
return [
|
|
@@ -330,7 +347,15 @@ function listRows(route, data, index, width, colors, density = "comfortable") {
|
|
|
330
347
|
let title = item.title || item.name || "",
|
|
331
348
|
meta = "",
|
|
332
349
|
body = "";
|
|
333
|
-
if (route.view === "
|
|
350
|
+
if (route.view === "projects") {
|
|
351
|
+
title = item.name;
|
|
352
|
+
meta = `${item.stage} · #${item.topic} · ${item.founder_name}`;
|
|
353
|
+
body = item.tagline || item.description;
|
|
354
|
+
} else if (route.view === "events") {
|
|
355
|
+
title = (item.cancelled_at ? "[Cancelled] " : "") + item.title;
|
|
356
|
+
meta = `${stamp(item.start_at)} · ${item.organizer_name}`;
|
|
357
|
+
body = item.description;
|
|
358
|
+
} else if (route.view === "drafts") {
|
|
334
359
|
title = item.form.values.title || item.form.title;
|
|
335
360
|
meta = "Local draft · " + stamp(item.savedAt);
|
|
336
361
|
body =
|
|
@@ -422,6 +447,8 @@ function listRows(route, data, index, width, colors, density = "comfortable") {
|
|
|
422
447
|
});
|
|
423
448
|
}
|
|
424
449
|
function documentRows(route, data, width, colors) {
|
|
450
|
+
if (route.view === "project" || route.view === "event")
|
|
451
|
+
return markdownRows(catalogDocument(route, data), width, colors);
|
|
425
452
|
if (route.view !== "post" || (route.tab && route.tab !== "overview"))
|
|
426
453
|
return contentLines(route, data).flatMap((line) => wrap(line, width));
|
|
427
454
|
const post = data?.post;
|
|
@@ -523,7 +550,8 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
523
550
|
),
|
|
524
551
|
[user, setUser] = useState(null),
|
|
525
552
|
[device, setDevice] = useState(null),
|
|
526
|
-
[authVersion, setAuthVersion] = useState(0)
|
|
553
|
+
[authVersion, setAuthVersion] = useState(0),
|
|
554
|
+
[booting, setBooting] = useState(true);
|
|
527
555
|
const [route, setRoute] = useState({ view: "feed", page: 1 }),
|
|
528
556
|
[data, setData] = useState(null),
|
|
529
557
|
[loading, setLoading] = useState(false),
|
|
@@ -693,6 +721,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
693
721
|
setPreview(null);
|
|
694
722
|
setDevice(null);
|
|
695
723
|
setUser(null);
|
|
724
|
+
setBooting(true);
|
|
696
725
|
setReading(null);
|
|
697
726
|
setActionIndex(null);
|
|
698
727
|
setUnread(0);
|
|
@@ -716,6 +745,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
716
745
|
} else setError(problemMessage(e));
|
|
717
746
|
}
|
|
718
747
|
function navigate(next, { replace = false } = {}) {
|
|
748
|
+
setBooting(false);
|
|
719
749
|
if (!replace)
|
|
720
750
|
history.current.push({
|
|
721
751
|
route,
|
|
@@ -800,6 +830,20 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
800
830
|
await queueDraft(() => drafts.remove(draftIdentity(value)));
|
|
801
831
|
}
|
|
802
832
|
async function restoreDraft(item) {
|
|
833
|
+
if (
|
|
834
|
+
item.form.kind === "catalog" &&
|
|
835
|
+
item.form.catalogProgress?.completedId
|
|
836
|
+
) {
|
|
837
|
+
navigate({
|
|
838
|
+
view: item.form.entity,
|
|
839
|
+
id: item.form.catalogProgress.completedId,
|
|
840
|
+
page: 1,
|
|
841
|
+
});
|
|
842
|
+
setNotice(
|
|
843
|
+
"This draft was already published. Opened the existing publication.",
|
|
844
|
+
);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
803
847
|
await task(async () => {
|
|
804
848
|
const saved = await drafts.read(item.id),
|
|
805
849
|
next = { ...saved.form, draftId: item.id };
|
|
@@ -838,6 +882,8 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
838
882
|
}
|
|
839
883
|
}
|
|
840
884
|
}
|
|
885
|
+
if (next.kind === "catalog")
|
|
886
|
+
next.topics = itemsOf(await api("/topics")).map((t) => [t.id, t.name]);
|
|
841
887
|
openForm(next);
|
|
842
888
|
setNotice(
|
|
843
889
|
"Restored local draft. Check the code selection before publishing.",
|
|
@@ -898,7 +944,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
898
944
|
if (!controller.signal.aborted) fail(e);
|
|
899
945
|
});
|
|
900
946
|
return () => controller.abort();
|
|
901
|
-
}, [phase, api]);
|
|
947
|
+
}, [phase, api, authVersion]);
|
|
902
948
|
useEffect(() => {
|
|
903
949
|
if (phase !== "ready" || !api || form || route.view === "help") return;
|
|
904
950
|
let active = true;
|
|
@@ -964,7 +1010,10 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
964
1010
|
if (active) fail(e);
|
|
965
1011
|
})
|
|
966
1012
|
.finally(() => {
|
|
967
|
-
if (active)
|
|
1013
|
+
if (active) {
|
|
1014
|
+
setLoading(false);
|
|
1015
|
+
setBooting(false);
|
|
1016
|
+
}
|
|
968
1017
|
});
|
|
969
1018
|
return () => {
|
|
970
1019
|
active = false;
|
|
@@ -996,6 +1045,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
996
1045
|
if (controller.signal.aborted) return;
|
|
997
1046
|
resetAccount();
|
|
998
1047
|
setConfig(saved);
|
|
1048
|
+
setBooting(true);
|
|
999
1049
|
setPhase("checking");
|
|
1000
1050
|
setNotice("Terminal connected.");
|
|
1001
1051
|
return;
|
|
@@ -1051,6 +1101,128 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
1051
1101
|
setCommandCursor(0);
|
|
1052
1102
|
}
|
|
1053
1103
|
}
|
|
1104
|
+
async function composeCatalog(entity, editing = false) {
|
|
1105
|
+
await task(async () => {
|
|
1106
|
+
const row = editing ? await api(`/${entity}s/${route.id}`) : {};
|
|
1107
|
+
if (editing && row.user_id !== user.id)
|
|
1108
|
+
throw Error("Only the owner can edit this publication.");
|
|
1109
|
+
const topics = itemsOf(await api("/topics")).map((t) => [t.id, t.name]);
|
|
1110
|
+
const values = initialCatalog(entity, row);
|
|
1111
|
+
if (!topics.some(([id]) => id === values.topic))
|
|
1112
|
+
values.topic = topics[0]?.[0] || "web";
|
|
1113
|
+
openForm({
|
|
1114
|
+
kind: "catalog",
|
|
1115
|
+
entity,
|
|
1116
|
+
entityId: row.id,
|
|
1117
|
+
revision: row.revision,
|
|
1118
|
+
existing: row,
|
|
1119
|
+
title: (editing ? "Edit your " : "Create a ") + entity,
|
|
1120
|
+
topics,
|
|
1121
|
+
values,
|
|
1122
|
+
});
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
async function catalogFilter() {
|
|
1126
|
+
await task(async () => {
|
|
1127
|
+
const entity = route.view === "projects" ? "project" : "event";
|
|
1128
|
+
const topics = itemsOf(await api("/topics")).map((t) => [t.id, t.name]);
|
|
1129
|
+
openForm({
|
|
1130
|
+
kind: "catalogFilter",
|
|
1131
|
+
entity,
|
|
1132
|
+
title: "Filter " + route.view,
|
|
1133
|
+
topics,
|
|
1134
|
+
values: {
|
|
1135
|
+
query: route.query || "",
|
|
1136
|
+
topic: route.topic || "",
|
|
1137
|
+
stage: route.stage || "",
|
|
1138
|
+
author: route.author || "",
|
|
1139
|
+
status: route.status || "",
|
|
1140
|
+
date: route.date || "",
|
|
1141
|
+
timezone: route.timezone || viewerZone(),
|
|
1142
|
+
mine: route.mine || "",
|
|
1143
|
+
order: route.order || (entity === "project" ? "newest" : "earliest"),
|
|
1144
|
+
},
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
function catalogAction(id) {
|
|
1149
|
+
const row = current?.project || current?.event;
|
|
1150
|
+
if (!row) return;
|
|
1151
|
+
if (id === "edit") {
|
|
1152
|
+
composeCatalog(route.view, true);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
if (id === "conversation") {
|
|
1156
|
+
navigate({ view: "post", id: row.post_id, tab: "overview", page: 1 });
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
if (id === "calendar") {
|
|
1160
|
+
openForm({
|
|
1161
|
+
kind: "calendar",
|
|
1162
|
+
eventId: row.id,
|
|
1163
|
+
title: "Export calendar",
|
|
1164
|
+
values: { path: `devdevian-event-${row.id}.ics` },
|
|
1165
|
+
});
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (id === "attend") {
|
|
1169
|
+
const attending = !row.viewer_attending;
|
|
1170
|
+
askConfirm(
|
|
1171
|
+
attending ? "Attend this event?" : "Leave this event?",
|
|
1172
|
+
row.title,
|
|
1173
|
+
() =>
|
|
1174
|
+
task(async () => {
|
|
1175
|
+
await api(`/events/${row.id}/attendance`, {
|
|
1176
|
+
method: "PUT",
|
|
1177
|
+
body: { attending },
|
|
1178
|
+
});
|
|
1179
|
+
invalidate(
|
|
1180
|
+
attending
|
|
1181
|
+
? "You're attending. Your browser account sees the same update."
|
|
1182
|
+
: "You left the event.",
|
|
1183
|
+
);
|
|
1184
|
+
}),
|
|
1185
|
+
);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (id === "lifecycle") {
|
|
1189
|
+
if (row.user_id !== user.id) {
|
|
1190
|
+
setError("Only the organizer can manage this event.");
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
const action = row.cancelled_at ? "restore" : "cancel";
|
|
1194
|
+
askConfirm(
|
|
1195
|
+
action === "cancel" ? "Cancel your event?" : "Restore your event?",
|
|
1196
|
+
"Attendees will receive the event update.",
|
|
1197
|
+
() =>
|
|
1198
|
+
task(async () => {
|
|
1199
|
+
await api(`/events/${row.id}/lifecycle`, {
|
|
1200
|
+
method: "POST",
|
|
1201
|
+
body: { action, revision: row.revision },
|
|
1202
|
+
});
|
|
1203
|
+
invalidate("Event updated.");
|
|
1204
|
+
}),
|
|
1205
|
+
);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
const target =
|
|
1209
|
+
id === "website"
|
|
1210
|
+
? row.url
|
|
1211
|
+
: id === "source"
|
|
1212
|
+
? row.repo
|
|
1213
|
+
: id === "meeting"
|
|
1214
|
+
? row.url
|
|
1215
|
+
: config.origin +
|
|
1216
|
+
(route.view === "project"
|
|
1217
|
+
? "/app#post/" + row.post_id
|
|
1218
|
+
: "/app#events/" + row.id);
|
|
1219
|
+
try {
|
|
1220
|
+
openBrowser(webURL(target));
|
|
1221
|
+
setNotice("Opened in your browser.");
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
setError(error.message);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1054
1226
|
function openAppearance() {
|
|
1055
1227
|
openForm({
|
|
1056
1228
|
kind: "appearance",
|
|
@@ -1062,6 +1234,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
1062
1234
|
);
|
|
1063
1235
|
}
|
|
1064
1236
|
function openForm(next) {
|
|
1237
|
+
setBooting(false);
|
|
1065
1238
|
setForm({
|
|
1066
1239
|
...next,
|
|
1067
1240
|
draftId: next.draftId || randomBytes(32).toString("hex"),
|
|
@@ -1225,6 +1398,28 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
1225
1398
|
}
|
|
1226
1399
|
async function prepare() {
|
|
1227
1400
|
if (!form) return;
|
|
1401
|
+
if (form.kind === "catalogFilter") {
|
|
1402
|
+
await task(async () => {
|
|
1403
|
+
const values = validateFilter(form.values);
|
|
1404
|
+
setForm(null);
|
|
1405
|
+
navigate({ view: form.entity + "s", ...values, page: 1 });
|
|
1406
|
+
});
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
if (form.kind === "calendar") {
|
|
1410
|
+
await task(async () => {
|
|
1411
|
+
const path = await saveCalendar(
|
|
1412
|
+
api,
|
|
1413
|
+
root,
|
|
1414
|
+
form.eventId,
|
|
1415
|
+
form.values.path,
|
|
1416
|
+
);
|
|
1417
|
+
setForm(null);
|
|
1418
|
+
setFocus("content");
|
|
1419
|
+
setNotice("Calendar saved: " + clean(path));
|
|
1420
|
+
});
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1228
1423
|
if (form.kind === "appearance") {
|
|
1229
1424
|
await task(async () => {
|
|
1230
1425
|
const saved = await saveAppearance(form.values);
|
|
@@ -1263,6 +1458,23 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
1263
1458
|
navigate({ view: "post", id, tab: "overview", page: 1 });
|
|
1264
1459
|
return;
|
|
1265
1460
|
}
|
|
1461
|
+
if (form.kind === "catalog") {
|
|
1462
|
+
if (form.catalogProgress?.uncertain)
|
|
1463
|
+
throw Error(
|
|
1464
|
+
"Publication outcome is unknown. Check your projects/events before creating another copy. Keep this draft for reference.",
|
|
1465
|
+
);
|
|
1466
|
+
const plan = await prepareCatalog(api, root, form);
|
|
1467
|
+
setPreview({
|
|
1468
|
+
catalog: plan,
|
|
1469
|
+
payload: plan.payload,
|
|
1470
|
+
title: form.title,
|
|
1471
|
+
method: form.entityId ? "PATCH" : "POST",
|
|
1472
|
+
url: `/${form.entity}s`,
|
|
1473
|
+
});
|
|
1474
|
+
setScroll(0);
|
|
1475
|
+
setYes(false);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1266
1478
|
let payload,
|
|
1267
1479
|
method = "POST",
|
|
1268
1480
|
url,
|
|
@@ -1354,10 +1566,22 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
1354
1566
|
async function publish() {
|
|
1355
1567
|
if (!preview) return;
|
|
1356
1568
|
await task(async () => {
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1569
|
+
let working = form;
|
|
1570
|
+
const response = preview.catalog
|
|
1571
|
+
? await publishCatalog(
|
|
1572
|
+
api,
|
|
1573
|
+
preview.catalog,
|
|
1574
|
+
form.catalogProgress,
|
|
1575
|
+
async (progress) => {
|
|
1576
|
+
working = { ...working, catalogProgress: progress };
|
|
1577
|
+
setForm(working);
|
|
1578
|
+
await queueDraft(() => drafts.save(working));
|
|
1579
|
+
},
|
|
1580
|
+
)
|
|
1581
|
+
: await api(preview.url, {
|
|
1582
|
+
method: preview.method,
|
|
1583
|
+
body: preview.payload,
|
|
1584
|
+
});
|
|
1361
1585
|
cache.current.clear();
|
|
1362
1586
|
let localWarning = "";
|
|
1363
1587
|
try {
|
|
@@ -1369,7 +1593,9 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
1369
1593
|
setPreview(null);
|
|
1370
1594
|
setForm(null);
|
|
1371
1595
|
setScroll(0);
|
|
1372
|
-
if (preview.
|
|
1596
|
+
if (preview.catalog)
|
|
1597
|
+
navigate({ view: form.entity, id: response.id, page: 1 });
|
|
1598
|
+
else if (preview.url === "/posts")
|
|
1373
1599
|
navigate({ view: "post", id: response.id, tab: "overview", page: 1 });
|
|
1374
1600
|
else if (preview.url === "/communities")
|
|
1375
1601
|
navigate({ view: "community", id: response.id, page: 1 });
|
|
@@ -1387,6 +1613,11 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
1387
1613
|
});
|
|
1388
1614
|
}
|
|
1389
1615
|
async function openSelected() {
|
|
1616
|
+
if (route.view === "projects" || route.view === "events") {
|
|
1617
|
+
if (selected)
|
|
1618
|
+
navigate({ view: route.view.slice(0, -1), id: selected.id, page: 1 });
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1390
1621
|
if (route.view === "community" && !current?.community?.is_member) return;
|
|
1391
1622
|
const item = selected;
|
|
1392
1623
|
if (!item) return;
|
|
@@ -1627,6 +1858,10 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
1627
1858
|
}
|
|
1628
1859
|
function runAction(id) {
|
|
1629
1860
|
setActionIndex(null);
|
|
1861
|
+
if (id?.startsWith("catalog-")) {
|
|
1862
|
+
catalogAction(id.slice(8));
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1630
1865
|
if (id === "read") {
|
|
1631
1866
|
if (current?.post) {
|
|
1632
1867
|
setReading(selected);
|
|
@@ -1685,6 +1920,10 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
1685
1920
|
);
|
|
1686
1921
|
return;
|
|
1687
1922
|
}
|
|
1923
|
+
if (parsed.command === "project" || parsed.command === "event") {
|
|
1924
|
+
navigate({ view: parsed.command, id: parsed.arg, page: 1 });
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1688
1927
|
if (parsed.command === "appearance") {
|
|
1689
1928
|
openAppearance();
|
|
1690
1929
|
return;
|
|
@@ -1702,14 +1941,16 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
1702
1941
|
navigate({
|
|
1703
1942
|
view: parsed.command,
|
|
1704
1943
|
page: 1,
|
|
1705
|
-
...(
|
|
1944
|
+
...(["search", "projects", "events"].includes(parsed.command)
|
|
1945
|
+
? { query: parsed.arg }
|
|
1946
|
+
: {}),
|
|
1706
1947
|
});
|
|
1707
1948
|
} catch (e) {
|
|
1708
1949
|
setError(e.message);
|
|
1709
1950
|
}
|
|
1710
1951
|
}
|
|
1711
1952
|
function askQuit() {
|
|
1712
|
-
if (
|
|
1953
|
+
if (["appearance", "catalogFilter", "calendar"].includes(form?.kind)) {
|
|
1713
1954
|
exit();
|
|
1714
1955
|
return;
|
|
1715
1956
|
}
|
|
@@ -1772,6 +2013,14 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
1772
2013
|
setDevice(null);
|
|
1773
2014
|
return;
|
|
1774
2015
|
}
|
|
2016
|
+
if (phase === "checking") {
|
|
2017
|
+
if (input === "q") exit();
|
|
2018
|
+
else if (error && (keyEvent.return || input === "r" || input === "R")) {
|
|
2019
|
+
setError("");
|
|
2020
|
+
setAuthVersion((v) => v + 1);
|
|
2021
|
+
}
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
1775
2024
|
if (input === "q") exit();
|
|
1776
2025
|
else if (keyEvent.return && !busy.current) {
|
|
1777
2026
|
if (phase === "pending" && device) openBrowser(device.verification_uri);
|
|
@@ -1848,6 +2097,22 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
1848
2097
|
return;
|
|
1849
2098
|
}
|
|
1850
2099
|
if (form) {
|
|
2100
|
+
if (keyEvent.ctrl && input === "o" && form.kind === "catalog") {
|
|
2101
|
+
const active = fieldsFor(form)[field];
|
|
2102
|
+
if (["logo", "banner", "screenshots"].includes(active?.key)) {
|
|
2103
|
+
setPicker({ mediaKey: active.key });
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
if (
|
|
2108
|
+
["catalogFilter", "calendar"].includes(form.kind) &&
|
|
2109
|
+
keyEvent.escape
|
|
2110
|
+
) {
|
|
2111
|
+
setForm(null);
|
|
2112
|
+
setFocus("content");
|
|
2113
|
+
setError("");
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
1851
2116
|
if (form.kind === "appearance" && keyEvent.escape) {
|
|
1852
2117
|
setForm(null);
|
|
1853
2118
|
setFocus("content");
|
|
@@ -1927,6 +2192,14 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
1927
2192
|
return;
|
|
1928
2193
|
}
|
|
1929
2194
|
if (keyEvent.return && !active.multiline) {
|
|
2195
|
+
if (
|
|
2196
|
+
form.kind === "catalog" &&
|
|
2197
|
+
["logo", "banner", "screenshots"].includes(active.key) &&
|
|
2198
|
+
!form.values[active.key]
|
|
2199
|
+
) {
|
|
2200
|
+
setPicker({ mediaKey: active.key });
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
1930
2203
|
if (active.key === "file" && !form.values.file) {
|
|
1931
2204
|
setPicker("files");
|
|
1932
2205
|
return;
|
|
@@ -2012,6 +2285,17 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2012
2285
|
setReviewFocus((v) => (v === "code" ? "notes" : "code"));
|
|
2013
2286
|
return;
|
|
2014
2287
|
}
|
|
2288
|
+
if (
|
|
2289
|
+
(input === "f" || input === "F") &&
|
|
2290
|
+
["projects", "events"].includes(route.view)
|
|
2291
|
+
) {
|
|
2292
|
+
catalogFilter();
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
if (input === "j" && route.view === "event") {
|
|
2296
|
+
catalogAction("attend");
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2015
2299
|
if (
|
|
2016
2300
|
reviewing &&
|
|
2017
2301
|
(input === "f" || input === "F") &&
|
|
@@ -2077,7 +2361,8 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2077
2361
|
const prose =
|
|
2078
2362
|
route.view === "help" ||
|
|
2079
2363
|
(route.view === "post" && (!route.tab || route.tab === "overview")) ||
|
|
2080
|
-
(route.view === "community" && !current?.community?.is_member)
|
|
2364
|
+
(route.view === "community" && !current?.community?.is_member) ||
|
|
2365
|
+
["project", "event"].includes(route.view);
|
|
2081
2366
|
if (
|
|
2082
2367
|
keyEvent.upArrow ||
|
|
2083
2368
|
keyEvent.downArrow ||
|
|
@@ -2149,6 +2434,10 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2149
2434
|
return;
|
|
2150
2435
|
}
|
|
2151
2436
|
if (input === "n") {
|
|
2437
|
+
if (["projects", "events"].includes(route.view)) {
|
|
2438
|
+
composeCatalog(route.view.slice(0, -1));
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2152
2441
|
if (route.view === "communities")
|
|
2153
2442
|
openForm({
|
|
2154
2443
|
kind: "community",
|
|
@@ -2177,6 +2466,10 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2177
2466
|
return;
|
|
2178
2467
|
}
|
|
2179
2468
|
if (input === "e" || input === "E") {
|
|
2469
|
+
if (["project", "event"].includes(route.view)) {
|
|
2470
|
+
composeCatalog(route.view, true);
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2180
2473
|
editSelected();
|
|
2181
2474
|
return;
|
|
2182
2475
|
}
|
|
@@ -2233,9 +2526,33 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2233
2526
|
root,
|
|
2234
2527
|
width,
|
|
2235
2528
|
height: bodyHeight,
|
|
2236
|
-
initial: picker,
|
|
2529
|
+
initial: picker.mediaKey ? "files" : picker,
|
|
2530
|
+
media: !!picker.mediaKey,
|
|
2237
2531
|
onCancel: () => setPicker(null),
|
|
2238
2532
|
onSelect: (source) => {
|
|
2533
|
+
if (picker.mediaKey) {
|
|
2534
|
+
const key = picker.mediaKey;
|
|
2535
|
+
setForm((old) => ({
|
|
2536
|
+
...old,
|
|
2537
|
+
values: {
|
|
2538
|
+
...old.values,
|
|
2539
|
+
[key]:
|
|
2540
|
+
key === "screenshots"
|
|
2541
|
+
? [
|
|
2542
|
+
old.values[key] === "-" ? "" : old.values[key],
|
|
2543
|
+
source.path,
|
|
2544
|
+
]
|
|
2545
|
+
.filter(Boolean)
|
|
2546
|
+
.join("\n")
|
|
2547
|
+
: source.path,
|
|
2548
|
+
},
|
|
2549
|
+
}));
|
|
2550
|
+
setPicker(null);
|
|
2551
|
+
setNotice(
|
|
2552
|
+
"Image selected locally. It is uploaded only after confirmation.",
|
|
2553
|
+
);
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2239
2556
|
setForm((old) => ({
|
|
2240
2557
|
...old,
|
|
2241
2558
|
source,
|
|
@@ -2257,44 +2574,82 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2257
2574
|
},
|
|
2258
2575
|
});
|
|
2259
2576
|
else if (phase !== "ready") {
|
|
2260
|
-
const
|
|
2261
|
-
|
|
2262
|
-
"Your conversations, without leaving the terminal.",
|
|
2263
|
-
"",
|
|
2264
|
-
...(phase === "pending" && device
|
|
2577
|
+
const statusLines =
|
|
2578
|
+
phase === "pending" && device
|
|
2265
2579
|
? [
|
|
2266
2580
|
"Confirm this code: " + device.user_code,
|
|
2267
|
-
"Waiting for your approval " + spin,
|
|
2268
2581
|
device.verification_uri,
|
|
2269
|
-
"
|
|
2582
|
+
"Waiting for your approval...",
|
|
2270
2583
|
]
|
|
2271
2584
|
: phase === "checking"
|
|
2272
|
-
?
|
|
2585
|
+
? error
|
|
2586
|
+
? ["Connection could not be completed.", error]
|
|
2587
|
+
: ["Connecting to Devdevian..."]
|
|
2273
2588
|
: [
|
|
2274
2589
|
"Sign in with your existing Devdevian account.",
|
|
2275
|
-
"",
|
|
2276
|
-
"Enter connects this terminal through your browser.",
|
|
2277
2590
|
"Google, GitHub and email sign-in stay on the website.",
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2591
|
+
];
|
|
2592
|
+
const footer =
|
|
2593
|
+
phase === "pending"
|
|
2594
|
+
? ["Enter reopens the browser · Esc cancels"]
|
|
2595
|
+
: phase === "checking"
|
|
2596
|
+
? [error ? "R or Enter retries · Ctrl+C exits" : "Ctrl+C exits"]
|
|
2597
|
+
: [
|
|
2598
|
+
"Enter connects this terminal through your browser.",
|
|
2599
|
+
"Q or Ctrl+C exits.",
|
|
2600
|
+
];
|
|
2282
2601
|
center = h(
|
|
2283
2602
|
Box,
|
|
2284
2603
|
{
|
|
2285
2604
|
borderStyle: colors.frame,
|
|
2286
2605
|
borderColor: colors.purple,
|
|
2287
2606
|
flexDirection: "column",
|
|
2288
|
-
paddingX:
|
|
2289
|
-
paddingY: 1,
|
|
2607
|
+
paddingX: 1,
|
|
2290
2608
|
height: bodyHeight,
|
|
2291
2609
|
overflow: "hidden",
|
|
2292
2610
|
},
|
|
2293
|
-
h(
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2611
|
+
h(DeviPanel, {
|
|
2612
|
+
width: Math.max(1, width - 4),
|
|
2613
|
+
height: Math.max(1, bodyHeight - 2),
|
|
2614
|
+
message:
|
|
2615
|
+
effectiveAppearance.welcome === "off"
|
|
2616
|
+
? ""
|
|
2617
|
+
: phase === "checking"
|
|
2618
|
+
? "Devi missed you."
|
|
2619
|
+
: "Devi saved you a seat.",
|
|
2620
|
+
status: statusLines,
|
|
2621
|
+
footer,
|
|
2622
|
+
show: effectiveAppearance.welcome === "on",
|
|
2623
|
+
animate: phase !== "auth" && !error,
|
|
2624
|
+
motion: effectiveAppearance.motion,
|
|
2625
|
+
}),
|
|
2626
|
+
);
|
|
2627
|
+
} else if (
|
|
2628
|
+
booting &&
|
|
2629
|
+
!current &&
|
|
2630
|
+
effectiveAppearance.welcome === "on" &&
|
|
2631
|
+
!error &&
|
|
2632
|
+
!dialog &&
|
|
2633
|
+
!form &&
|
|
2634
|
+
!picker
|
|
2635
|
+
) {
|
|
2636
|
+
center = h(
|
|
2637
|
+
Box,
|
|
2638
|
+
{
|
|
2639
|
+
borderStyle: colors.frame,
|
|
2640
|
+
borderColor: colors.purple,
|
|
2641
|
+
paddingX: 1,
|
|
2642
|
+
height: bodyHeight,
|
|
2643
|
+
overflow: "hidden",
|
|
2644
|
+
},
|
|
2645
|
+
h(DeviPanel, {
|
|
2646
|
+
width: Math.max(1, width - 4),
|
|
2647
|
+
height: Math.max(1, bodyHeight - 2),
|
|
2648
|
+
message: "Devi missed you.",
|
|
2649
|
+
status: ["Loading your " + labelOf(route).toLowerCase() + "..."],
|
|
2650
|
+
footer: ["Ctrl+C exits"],
|
|
2651
|
+
motion: effectiveAppearance.motion,
|
|
2652
|
+
animate: true,
|
|
2298
2653
|
}),
|
|
2299
2654
|
);
|
|
2300
2655
|
} else if (dialog) {
|
|
@@ -2361,8 +2716,29 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2361
2716
|
const raw = [
|
|
2362
2717
|
"AUDIENCE · " + audience,
|
|
2363
2718
|
"",
|
|
2364
|
-
...(payload.title
|
|
2719
|
+
...(payload.title || payload.name
|
|
2720
|
+
? [payload.title || payload.name, ""]
|
|
2721
|
+
: []),
|
|
2365
2722
|
payload.body || payload.reason || payload.description || "",
|
|
2723
|
+
...(preview.catalog
|
|
2724
|
+
? [
|
|
2725
|
+
"",
|
|
2726
|
+
...Object.entries(payload)
|
|
2727
|
+
.filter(([key]) => !["description", "details"].includes(key))
|
|
2728
|
+
.map(([key, value]) => `${key}: ${value}`),
|
|
2729
|
+
"",
|
|
2730
|
+
"DETAILS",
|
|
2731
|
+
JSON.stringify(payload.details, null, 2),
|
|
2732
|
+
"",
|
|
2733
|
+
"IMAGES TO UPLOAD",
|
|
2734
|
+
...(preview.catalog.files.length
|
|
2735
|
+
? preview.catalog.files.map(
|
|
2736
|
+
(file) =>
|
|
2737
|
+
`${file.path} · ${file.mime} · ${file.size} bytes`,
|
|
2738
|
+
)
|
|
2739
|
+
: ["No new images"]),
|
|
2740
|
+
]
|
|
2741
|
+
: []),
|
|
2366
2742
|
...(payload.code ? ["", "CODE TO PUBLISH", payload.code] : []),
|
|
2367
2743
|
...(payload.review_focus
|
|
2368
2744
|
? ["", "REVIEW FOCUS", payload.review_focus]
|
|
@@ -2405,7 +2781,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2405
2781
|
width: contentWidth,
|
|
2406
2782
|
height: contentHeight,
|
|
2407
2783
|
});
|
|
2408
|
-
else if (loading && !current)
|
|
2784
|
+
else if ((loading || booting) && !current)
|
|
2409
2785
|
body = h(
|
|
2410
2786
|
Text,
|
|
2411
2787
|
{ color: colors.muted },
|
|
@@ -2500,30 +2876,49 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2500
2876
|
height: contentHeight,
|
|
2501
2877
|
offset: Math.min(scroll, Math.max(0, lines.length - contentHeight)),
|
|
2502
2878
|
});
|
|
2503
|
-
else if (!current?.items?.length)
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2879
|
+
else if (!current?.items?.length) {
|
|
2880
|
+
const messages = {
|
|
2881
|
+
inbox: [
|
|
2882
|
+
"You're all caught up.",
|
|
2883
|
+
"Replies, mentions and review updates will appear here.",
|
|
2884
|
+
],
|
|
2885
|
+
drafts: [
|
|
2886
|
+
"No local drafts yet.",
|
|
2887
|
+
"Start writing to save a private draft on this device.",
|
|
2888
|
+
],
|
|
2889
|
+
communities: [
|
|
2890
|
+
"Find your people.",
|
|
2891
|
+
"Use / to search or N to create a private community.",
|
|
2892
|
+
],
|
|
2893
|
+
feed: [
|
|
2894
|
+
"Nothing here yet.",
|
|
2895
|
+
"N starts a conversation. A asks a question.",
|
|
2896
|
+
],
|
|
2897
|
+
search: route.query
|
|
2898
|
+
? [
|
|
2899
|
+
"No conversations matched your search.",
|
|
2900
|
+
"Try fewer words or a different phrase.",
|
|
2901
|
+
]
|
|
2902
|
+
: [
|
|
2903
|
+
"Find your next useful conversation.",
|
|
2904
|
+
"Press / and type /search followed by your query.",
|
|
2905
|
+
],
|
|
2906
|
+
};
|
|
2907
|
+
const message = messages[route.view] || [
|
|
2908
|
+
"Nothing here yet.",
|
|
2909
|
+
"Use / to search or Tab to explore another section.",
|
|
2910
|
+
];
|
|
2911
|
+
body = h(DeviPanel, {
|
|
2912
|
+
width: contentWidth,
|
|
2913
|
+
height: contentHeight,
|
|
2914
|
+
message: message[0],
|
|
2915
|
+
status: [message[1]],
|
|
2916
|
+
show:
|
|
2917
|
+
effectiveAppearance.welcome === "on" &&
|
|
2918
|
+
Object.hasOwn(messages, route.view),
|
|
2919
|
+
motion: effectiveAppearance.motion,
|
|
2920
|
+
});
|
|
2921
|
+
} else {
|
|
2527
2922
|
const visible = Math.max(1, Math.floor(contentHeight / stride)),
|
|
2528
2923
|
first = Math.max(0, index - visible + 1),
|
|
2529
2924
|
rows = listRows(
|
|
@@ -2702,27 +3097,31 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2702
3097
|
);
|
|
2703
3098
|
}
|
|
2704
3099
|
const hint =
|
|
2705
|
-
form
|
|
2706
|
-
? "
|
|
2707
|
-
:
|
|
2708
|
-
? "
|
|
2709
|
-
:
|
|
2710
|
-
? "
|
|
2711
|
-
:
|
|
2712
|
-
? "
|
|
2713
|
-
: reading
|
|
2714
|
-
? "
|
|
2715
|
-
:
|
|
2716
|
-
? "
|
|
2717
|
-
:
|
|
2718
|
-
? "
|
|
2719
|
-
:
|
|
2720
|
-
? "
|
|
2721
|
-
:
|
|
2722
|
-
? "
|
|
2723
|
-
:
|
|
2724
|
-
? "
|
|
2725
|
-
:
|
|
3100
|
+
["projects", "events"].includes(route.view) && !form && !preview && !picker
|
|
3101
|
+
? "↑↓ select · Enter open · F filters · N new · [ ] pages · / search"
|
|
3102
|
+
: ["project", "event"].includes(route.view) && !form && !preview
|
|
3103
|
+
? "↑↓ scroll · X actions · E edit your publication · Esc back"
|
|
3104
|
+
: form?.kind === "appearance"
|
|
3105
|
+
? "← → choose · Tab next · Ctrl+S save · Esc cancel · 0 default preview"
|
|
3106
|
+
: actionIndex !== null
|
|
3107
|
+
? "Actions are scoped to the selected item · Esc closes"
|
|
3108
|
+
: reviewing && !form && !preview && !reading
|
|
3109
|
+
? "W code / discussion · Space select · C comment · X actions · F filter"
|
|
3110
|
+
: picker
|
|
3111
|
+
? "Project files stay local until publication is confirmed"
|
|
3112
|
+
: reading
|
|
3113
|
+
? "↑↓ scroll · R reply · E edit · D delete · Esc close"
|
|
3114
|
+
: preview
|
|
3115
|
+
? "← → choose · Enter confirm · Esc edit"
|
|
3116
|
+
: form
|
|
3117
|
+
? "Tab field · Ctrl+E editor · Ctrl+O files · Ctrl+G diff · Ctrl+S preview"
|
|
3118
|
+
: phase !== "ready"
|
|
3119
|
+
? "Enter connect / open browser · Ctrl+C exit"
|
|
3120
|
+
: route.view === "feedback"
|
|
3121
|
+
? "↑↓ select · R reply · E edit · D delete · S resolve · ! report"
|
|
3122
|
+
: route.view === "post"
|
|
3123
|
+
? "←→ tabs · ↑↓ select · R reply · E edit · D delete · Space read"
|
|
3124
|
+
: "↑↓ select · Enter open · Tab sections · [ ] pages · N new · / search";
|
|
2726
3125
|
return h(
|
|
2727
3126
|
ThemeContext.Provider,
|
|
2728
3127
|
{ value: colors },
|