@wix/himalaya-cli 0.1411.0 → 0.1412.0

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.
Files changed (85) hide show
  1. package/dist/cli.mjs +1174 -790
  2. package/guides/release-notes.md +12 -0
  3. package/guides/wix-business-context.md +32 -0
  4. package/guides/wix-site-to-himi-app.md +21 -3
  5. package/package.json +2 -2
  6. package/templates/beauty-salon-owner/config.ts +29 -0
  7. package/templates/beauty-salon-owner/dev/index.ts +1121 -0
  8. package/templates/beauty-salon-owner/himalaya.content.json +6 -0
  9. package/templates/beauty-salon-owner/tier5-src/_shared/chat-live.ts +242 -0
  10. package/templates/beauty-salon-owner/tier5-src/_shared/detail-presence.ts +51 -0
  11. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-inbox/index.ts +308 -0
  12. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-inbox-thread/index.ts +307 -0
  13. package/templates/branded-lite/dev/index.ts +1 -0
  14. package/templates/branded-lite/tier5-src/_shared/chat-live.ts +242 -0
  15. package/templates/branded-lite/tier5-src/wix-spaces-chat-thread/index.ts +131 -21
  16. package/templates/community-club-owner/config.ts +29 -0
  17. package/templates/community-club-owner/dev/index.ts +1121 -0
  18. package/templates/community-club-owner/himalaya.content.json +6 -0
  19. package/templates/community-club-owner/tier5-src/_shared/chat-live.ts +242 -0
  20. package/templates/community-club-owner/tier5-src/_shared/detail-presence.ts +51 -0
  21. package/templates/community-club-owner/tier5-src/himi-owner-lite-inbox/index.ts +308 -0
  22. package/templates/community-club-owner/tier5-src/himi-owner-lite-inbox-thread/index.ts +307 -0
  23. package/templates/events-meetups-owner/config.ts +29 -0
  24. package/templates/events-meetups-owner/dev/index.ts +1121 -0
  25. package/templates/events-meetups-owner/himalaya.content.json +6 -0
  26. package/templates/events-meetups-owner/tier5-src/_shared/chat-live.ts +242 -0
  27. package/templates/events-meetups-owner/tier5-src/_shared/detail-presence.ts +51 -0
  28. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-inbox/index.ts +308 -0
  29. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-inbox-thread/index.ts +307 -0
  30. package/templates/gym-fitness-owner/config.ts +29 -0
  31. package/templates/gym-fitness-owner/dev/index.ts +1121 -0
  32. package/templates/gym-fitness-owner/himalaya.content.json +6 -0
  33. package/templates/gym-fitness-owner/tier5-src/_shared/chat-live.ts +242 -0
  34. package/templates/gym-fitness-owner/tier5-src/_shared/detail-presence.ts +51 -0
  35. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-inbox/index.ts +308 -0
  36. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-inbox-thread/index.ts +307 -0
  37. package/templates/nonprofit-cause-owner/config.ts +29 -0
  38. package/templates/nonprofit-cause-owner/dev/index.ts +1121 -0
  39. package/templates/nonprofit-cause-owner/himalaya.content.json +6 -0
  40. package/templates/nonprofit-cause-owner/tier5-src/_shared/chat-live.ts +242 -0
  41. package/templates/nonprofit-cause-owner/tier5-src/_shared/detail-presence.ts +51 -0
  42. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-inbox/index.ts +308 -0
  43. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-inbox-thread/index.ts +307 -0
  44. package/templates/owner-lite/config.ts +29 -0
  45. package/templates/owner-lite/dev/index.ts +1121 -0
  46. package/templates/owner-lite/himalaya.content.json +6 -0
  47. package/templates/owner-lite/tier5-src/_shared/chat-live.ts +242 -0
  48. package/templates/owner-lite/tier5-src/_shared/detail-presence.ts +51 -0
  49. package/templates/owner-lite/tier5-src/himi-owner-lite-inbox/index.ts +308 -0
  50. package/templates/owner-lite/tier5-src/himi-owner-lite-inbox-thread/index.ts +307 -0
  51. package/templates/podcast-creator-owner/config.ts +29 -0
  52. package/templates/podcast-creator-owner/dev/index.ts +1121 -0
  53. package/templates/podcast-creator-owner/himalaya.content.json +6 -0
  54. package/templates/podcast-creator-owner/tier5-src/_shared/chat-live.ts +242 -0
  55. package/templates/podcast-creator-owner/tier5-src/_shared/detail-presence.ts +51 -0
  56. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-inbox/index.ts +308 -0
  57. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-inbox-thread/index.ts +307 -0
  58. package/templates/portfolio-creative-owner/config.ts +29 -0
  59. package/templates/portfolio-creative-owner/dev/index.ts +1121 -0
  60. package/templates/portfolio-creative-owner/himalaya.content.json +6 -0
  61. package/templates/portfolio-creative-owner/tier5-src/_shared/chat-live.ts +242 -0
  62. package/templates/portfolio-creative-owner/tier5-src/_shared/detail-presence.ts +51 -0
  63. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-inbox/index.ts +308 -0
  64. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-inbox-thread/index.ts +307 -0
  65. package/templates/restaurant-owner/config.ts +29 -0
  66. package/templates/restaurant-owner/dev/index.ts +1121 -0
  67. package/templates/restaurant-owner/himalaya.content.json +6 -0
  68. package/templates/restaurant-owner/tier5-src/_shared/chat-live.ts +242 -0
  69. package/templates/restaurant-owner/tier5-src/_shared/detail-presence.ts +51 -0
  70. package/templates/restaurant-owner/tier5-src/himi-owner-lite-inbox/index.ts +308 -0
  71. package/templates/restaurant-owner/tier5-src/himi-owner-lite-inbox-thread/index.ts +307 -0
  72. package/templates/retail-store-owner/config.ts +29 -0
  73. package/templates/retail-store-owner/dev/index.ts +1121 -0
  74. package/templates/retail-store-owner/himalaya.content.json +6 -0
  75. package/templates/retail-store-owner/tier5-src/_shared/chat-live.ts +242 -0
  76. package/templates/retail-store-owner/tier5-src/_shared/detail-presence.ts +51 -0
  77. package/templates/retail-store-owner/tier5-src/himi-owner-lite-inbox/index.ts +308 -0
  78. package/templates/retail-store-owner/tier5-src/himi-owner-lite-inbox-thread/index.ts +307 -0
  79. package/templates/yoga-studio-owner/config.ts +29 -0
  80. package/templates/yoga-studio-owner/dev/index.ts +1121 -0
  81. package/templates/yoga-studio-owner/himalaya.content.json +6 -0
  82. package/templates/yoga-studio-owner/tier5-src/_shared/chat-live.ts +242 -0
  83. package/templates/yoga-studio-owner/tier5-src/_shared/detail-presence.ts +51 -0
  84. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-inbox/index.ts +308 -0
  85. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-inbox-thread/index.ts +307 -0
@@ -0,0 +1,307 @@
1
+ // Tier-5 bundle: __HIMI_APP_NAME__ — Inbox thread + reply.
2
+ //
3
+ // `himalaya.input` carries `id` and `kind`:
4
+ // kind "chatroom" — a chat-web chatroom id, from an Inbox row; used as-is.
5
+ // kind "contact" — a Wix contact id, from Ping's `inbox/conversation/:contactId`
6
+ // notification. Resolved to the conversation whose participant is that
7
+ // contact; when none is found the screen goes back to the Inbox list it
8
+ // is stacked on, which is a better place to land than "not found" for a message the
9
+ // owner was just told about.
10
+ //
11
+ // Reads and the composer are chat-web (`getMessages`, `sendChatMessage`), lifted from the
12
+ // wix-owner-clone app's `wix-owner-inbox-thread`. Keeping the thread live while it is open
13
+ // is the shared `_shared/chat-live.ts`: a `wix-realtime://` stream when the screen is given
14
+ // a channel (`chatRealtime` appear param), else a 4 s foreground re-read. Either way a
15
+ // change is answered by re-reading the thread, never by trusting an event's payload.
16
+ //
17
+ // Honest states: a missing id is the not-found state with no composer; a failed read is the
18
+ // load-error state — never an empty conversation whose Send can only fail. A failed send
19
+ // keeps the draft and says why.
20
+
21
+ import { defineService, navigate, type NavDirective, type PollDirective, type StreamDirective } from "@himalaya/state";
22
+ import { getMessages, sendChatMessage, listChatrooms, markChatroomRead, getChatToken, type ChatMessage } from "@himalaya/wix-chat-web";
23
+ import { formatMonthDayTime } from "../_shared/engine-intl.js";
24
+ import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
25
+ import { ownerLoadError, ownerWriteError, writeMayHaveLanded } from "../_shared/owner-load-error.js";
26
+ import { stashCopy, copyParam } from "../_shared/himi-copy.js";
27
+ import { chatRealtimeConfig, createReloadGate, createSendIntents, createVisitToken, liveDirectives, postWithVisitToken, liveFallback, type ChatLiveMode, type ChatRealtimeConfig } from "../_shared/chat-live.js";
28
+
29
+ const SERVICE = "ownerLiteInboxThread";
30
+ const PAGE_SIZE = 50;
31
+ /** How far back a contact → conversation lookup reads. */
32
+ const LOOKUP_SIZE = 100;
33
+
34
+ export interface Bubble {
35
+ id: string;
36
+ text: string;
37
+ when: string;
38
+ /** " " for the owner's own messages (a gated flexible spacer pushes the bubble right),
39
+ * "" for the customer's (the spacer collapses, the bubble stays left). */
40
+ leadFill: string;
41
+ mine: boolean;
42
+ theirs: boolean;
43
+ spokenLabel: string;
44
+ }
45
+
46
+ export interface ThreadState extends PresenceFlags {
47
+ status: "loading" | "ready" | "error";
48
+ phase: "loading" | "ready";
49
+ chatroomId: string;
50
+ headerName: string;
51
+ messages: Bubble[];
52
+ hideEmpty: boolean;
53
+ error: string;
54
+ draft: string;
55
+ sendHint: string;
56
+ sending: boolean;
57
+ live: ChatLiveMode;
58
+ _stream?: StreamDirective;
59
+ _poll?: PollDirective;
60
+ _nav?: NavDirective;
61
+ }
62
+
63
+ function input(): { id: string; kind: string } {
64
+ const i = (globalThis as { himalaya?: { input?: { kind?: unknown } } }).himalaya?.input;
65
+ return { id: detailInputId(), kind: typeof i?.kind === "string" ? i.kind : "" };
66
+ }
67
+
68
+ /** chat-web senders: business / businessUser / user are the owner's side. */
69
+ export function isBusiness(m: ChatMessage): boolean {
70
+ const t = m.sender?.type || "";
71
+ return t === "business" || t === "businessUser" || t === "user";
72
+ }
73
+
74
+ function textOf(m: ChatMessage): string {
75
+ const part = (m.data ?? []).find((p) => (p.mimeType || "").startsWith("text"));
76
+ return (part?.data?.text || m.summary || "").trim();
77
+ }
78
+
79
+ export function bubbles(msgs: ChatMessage[]): Bubble[] {
80
+ // chat-web answers newest-first by default; a thread reads oldest at the top.
81
+ const ordered = [...msgs].sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0));
82
+ const you = copyParam("ownerInbox.you", "You");
83
+ return ordered
84
+ .map((m, i) => {
85
+ const mine = isBusiness(m);
86
+ const text = textOf(m);
87
+ const when = m.createdAt ? formatMonthDayTime(new Date(m.createdAt)) : "";
88
+ const who = mine ? you : (m.senderDisplayData?.name || "").trim();
89
+ return { id: m.id || `m${i}`, text, when, leadFill: mine ? " " : "", mine, theirs: !mine, spokenLabel: [who, text, when].filter(Boolean).join(", ") };
90
+ })
91
+ .filter((b) => b.text);
92
+ }
93
+
94
+ /** The newest message's sequence — what "read up to here" names. */
95
+ function newestSequence(msgs: ChatMessage[]): number | undefined {
96
+ let max: number | undefined;
97
+ for (const m of msgs) if (typeof m.sequence === "number" && Number.isSafeInteger(m.sequence) && (max === undefined || m.sequence > max)) max = m.sequence;
98
+ return max;
99
+ }
100
+
101
+ function headerOf(msgs: ChatMessage[], fallback: string): string {
102
+ const name = msgs.find((m) => !isBusiness(m))?.senderDisplayData?.name?.trim();
103
+ return name || fallback || copyParam("ownerInbox.untitled", "Conversation");
104
+ }
105
+
106
+ /** The conversation a contact is in, or "" when the owner's recent conversations have none. */
107
+ async function chatroomForContact(contactId: string): Promise<{ id: string; name: string }> {
108
+ const rooms = await listChatrooms(LOOKUP_SIZE);
109
+ const room = rooms.find((r) => r?.id && r.participantId === contactId);
110
+ return room ? { id: room.id, name: (room.displayData?.name || "").trim() } : { id: "", name: "" };
111
+ }
112
+
113
+ // The conversation the gate re-reads. Module state, not worker state: a re-read must use
114
+ // the conversation this screen opened even if an older state object is in flight.
115
+ let current = "";
116
+ let realtime: ChatRealtimeConfig | null = null;
117
+
118
+ // ONE chatToken per visit (`_shared/chat-live.ts`): a 4 s re-read would otherwise mint a
119
+ // token every 4 s on top of its GET. Reset on every open.
120
+ const visitToken = createVisitToken(() => getChatToken().then((t) => t.chatToken));
121
+ async function readThread(): Promise<ChatMessage[]> {
122
+ const token = visitToken.get();
123
+ try {
124
+ return await getMessages(current, PAGE_SIZE, { chatToken: await token });
125
+ } catch (e) {
126
+ visitToken.drop(token);
127
+ throw e;
128
+ }
129
+ }
130
+
131
+ /** Send ids that belong to the intent (conversation + text), not the tap. */
132
+ const intents = createSendIntents();
133
+
134
+ /** Post once per intent: a retried tap of the same text reuses its message id. */
135
+ async function postOnce(chatroomId: string, text: string): Promise<void> {
136
+ const messageId = intents.idFor(chatroomId, text);
137
+ await postWithVisitToken(visitToken, (chatToken) => sendChatMessage(chatroomId, text, { chatToken, messageId }));
138
+ intents.settle(chatroomId, text);
139
+ }
140
+
141
+ const gate = createReloadGate(readThread);
142
+
143
+ /** The newest sequence already marked read, per conversation. A re-read every 4 s must
144
+ * not become a write every 4 s: only a message newer than this is marked. Reset on every
145
+ * open — the owner may have marked the conversation unread from the list in between. */
146
+ const markedUpTo = new Map<string, number>();
147
+
148
+ /** Tell chat-web the owner has read the thread, so the Inbox badge clears on return.
149
+ * Best effort: a failure here costs a stale badge, never the thread — and forgets the
150
+ * mark, so the next read tries again. */
151
+ function markRead(chatroomId: string, msgs: ChatMessage[]): void {
152
+ const seq = newestSequence(msgs);
153
+ if (seq === undefined || seq <= (markedUpTo.get(chatroomId) ?? -1)) return;
154
+ markedUpTo.set(chatroomId, seq);
155
+ void visitToken.get()
156
+ .then((t) => markChatroomRead(chatroomId, seq, { chatToken: t }))
157
+ .catch(() => {
158
+ if (markedUpTo.get(chatroomId) === seq) markedUpTo.delete(chatroomId);
159
+ });
160
+ }
161
+
162
+ function withMessages(s: ThreadState, msgs: ChatMessage[]): ThreadState {
163
+ const messages = bubbles(msgs);
164
+ return { ...s, messages, hideEmpty: messages.length > 0, headerName: headerOf(msgs, s.headerName), error: "" };
165
+ }
166
+
167
+ /**
168
+ * The state to put a finished read onto. NOT the state the action started with: the
169
+ * runtime replaces the whole service state with what an action returns, so a read that
170
+ * resolves after the owner typed or sent would otherwise put back the old draft and the
171
+ * old messages. Reading the live state after the `await` keeps everything that changed
172
+ * meanwhile and replaces only what the read is the answer for.
173
+ */
174
+ function live(): ThreadState {
175
+ return svc.getState();
176
+ }
177
+
178
+ /** A tick or an event: re-read, keep the screen as it is when the read fails (the next
179
+ * tick tries again), and do nothing before the conversation is open. */
180
+ async function reread(s: ThreadState): Promise<ThreadState> {
181
+ if (!s.chatroomId || !s.found || s.chatroomId !== current) return s;
182
+ try {
183
+ const msgs = await gate.request();
184
+ const now = live();
185
+ if (now.chatroomId !== s.chatroomId) return now;
186
+ markRead(s.chatroomId, msgs);
187
+ return withMessages(now, msgs);
188
+ } catch {
189
+ return live();
190
+ }
191
+ }
192
+
193
+ /** A send in flight. Set synchronously, before the first `await`, so a second tap cannot
194
+ * start a second send: an async action publishes no state until it returns, so a state
195
+ * flag could not guard this. */
196
+ let sendingNow = false;
197
+
198
+ const INITIAL: ThreadState = {
199
+ ...presence("loading"),
200
+ status: "loading",
201
+ phase: "loading",
202
+ chatroomId: "",
203
+ headerName: "",
204
+ messages: [],
205
+ hideEmpty: true,
206
+ error: "",
207
+ draft: "",
208
+ sendHint: "",
209
+ sending: false,
210
+ live: "off",
211
+ };
212
+
213
+ const svc = defineService<ThreadState, {
214
+ load: (s: ThreadState, arg?: unknown) => Promise<ThreadState>;
215
+ refresh: (s: ThreadState) => Promise<ThreadState>;
216
+ onStreamEvent: (s: ThreadState) => Promise<ThreadState>;
217
+ onStreamError: (s: ThreadState) => ThreadState;
218
+ setDraft: (s: ThreadState, arg?: unknown) => ThreadState;
219
+ send: (s: ThreadState) => Promise<ThreadState>;
220
+ back: (s: ThreadState) => ThreadState;
221
+ }>(SERVICE, {
222
+ state: INITIAL,
223
+ actions: {
224
+ load: async (s, arg) => {
225
+ stashCopy(arg);
226
+ const param = (arg as { chatRealtime?: unknown } | null | undefined)?.chatRealtime;
227
+ if (param !== undefined) realtime = chatRealtimeConfig(param);
228
+ const { id, kind } = input();
229
+ if (!id) return { ...s, ...presence("missing"), status: "ready", phase: "ready", messages: [], hideEmpty: true, live: "off" };
230
+ visitToken.reset();
231
+
232
+ let chatroomId = id;
233
+ let headerName = s.headerName;
234
+ if (kind === "contact") {
235
+ try {
236
+ const room = await chatroomForContact(id);
237
+ // No conversation for that contact among the recent ones: go BACK to the Inbox the
238
+ // thread is stacked on (a push synthesizes Inbox → thread). A `replace` would leave
239
+ // Inbox → Inbox, with a Back to an identical list.
240
+ if (!room.id) return navigate({ ...live(), status: "ready", phase: "ready" }, { presentation: "back" });
241
+ chatroomId = room.id;
242
+ headerName = room.name || headerName;
243
+ } catch (e) {
244
+ return { ...live(), ...presence("failed"), status: "error", phase: "ready", error: ownerLoadError(e), live: "off" };
245
+ }
246
+ }
247
+
248
+ current = chatroomId;
249
+ markedUpTo.delete(chatroomId);
250
+ try {
251
+ const msgs = await readThread();
252
+ markRead(chatroomId, msgs);
253
+ const next = withMessages({ ...live(), ...presence("found"), status: "ready", phase: "ready", chatroomId, headerName }, msgs);
254
+ return { ...next, ...liveDirectives(realtime, chatroomId, SERVICE) };
255
+ } catch (e) {
256
+ return { ...live(), ...presenceAfterError(e), status: "error", phase: "ready", chatroomId, headerName, messages: [], hideEmpty: true, error: ownerLoadError(e), live: "off" };
257
+ }
258
+ },
259
+ // The fallback poll's tick.
260
+ refresh: async (s) => reread(s),
261
+ // "The thread changed" — the payload is deliberately not read.
262
+ onStreamEvent: async (s) => reread(s),
263
+ // The channel is gone (or this host refuses it, as the web host does): poll instead.
264
+ onStreamError: (s) => (s.chatroomId ? { ...s, ...liveFallback(SERVICE) } : s),
265
+ setDraft: (s, arg) => {
266
+ const v = typeof arg === "string" ? arg : (arg as { value?: unknown } | null | undefined)?.value;
267
+ return { ...s, draft: typeof v === "string" ? v : "", sendHint: "" };
268
+ },
269
+ // "Back to Inbox" from the not-found state: pop to the Inbox the thread is stacked on.
270
+ // `himi.open` would push a second Inbox, and Back would then return to the dead thread.
271
+ back: (s) => navigate(s, { presentation: "back" }),
272
+ send: async (s) => {
273
+ const text = s.draft.trim();
274
+ if (!text) return { ...s, sendHint: copyParam("ownerInbox.typeFirst", "Type a message first.") };
275
+ if (!s.chatroomId || !s.found || sendingNow) return s;
276
+ sendingNow = true;
277
+ try {
278
+ try {
279
+ await postOnce(s.chatroomId, text);
280
+ } catch (e) {
281
+ // The draft stays: the owner should not have to type it again. A send that may
282
+ // have landed says so, so a retry does not post it twice unknowingly.
283
+ return { ...live(), sendHint: writeMayHaveLanded(e) ? ownerWriteError(e) : `${copyParam("ownerInbox.sendFailed", "Couldn’t send")} — ${ownerWriteError(e)}` };
284
+ }
285
+ // Clear the composer only if it still holds what was sent — the owner may already be
286
+ // typing the next message — judged on the state AFTER the re-read's await.
287
+ const cleared = (now: ThreadState): string => (now.draft.trim() === text ? "" : now.draft);
288
+ try {
289
+ const msgs = await gate.request();
290
+ const now = live();
291
+ return withMessages({ ...now, draft: cleared(now), sendHint: "" }, msgs);
292
+ } catch {
293
+ // Sent, but the re-read failed: show it locally until the next tick replaces it.
294
+ const you = copyParam("ownerInbox.you", "You");
295
+ const now = live();
296
+ const mine: Bubble = { id: `local-${now.messages.length}`, text, when: "", leadFill: " ", mine: true, theirs: false, spokenLabel: `${you}, ${text}` };
297
+ return { ...now, draft: cleared(now), sendHint: "", messages: [...now.messages, mine], hideEmpty: true };
298
+ }
299
+ } finally {
300
+ sendingNow = false;
301
+ }
302
+ },
303
+ },
304
+ });
305
+
306
+ svc.load();
307
+ export { svc };
@@ -78,6 +78,23 @@ export default defineApp({
78
78
  "parentScreenId": "",
79
79
  "tabId": "manage"
80
80
  },
81
+ {
82
+ "screenId": "himi_owner_lite_inbox",
83
+ "isPublic": false,
84
+ "paramKeys": [],
85
+ "parentScreenId": "",
86
+ "tabId": "inbox"
87
+ },
88
+ {
89
+ "screenId": "himi_owner_lite_inbox_thread",
90
+ "isPublic": false,
91
+ "paramKeys": [
92
+ "id",
93
+ "kind"
94
+ ],
95
+ "parentScreenId": "himi_owner_lite_inbox",
96
+ "tabId": "inbox"
97
+ },
81
98
  {
82
99
  "screenId": "himi_owner_lite_search",
83
100
  "isPublic": false,
@@ -508,6 +525,12 @@ export default defineApp({
508
525
  "icon": "home",
509
526
  "selectedIcon": "home_fill"
510
527
  },
528
+ {
529
+ "id": "inbox",
530
+ "screenId": "himi_owner_lite_inbox",
531
+ "label": "Inbox",
532
+ "icon": "chat"
533
+ },
511
534
  {
512
535
  "id": "aria",
513
536
  "screenId": "himi_owner_lite_aria",
@@ -532,6 +555,12 @@ export default defineApp({
532
555
  "icon": "home",
533
556
  "selectedIcon": "home_fill"
534
557
  },
558
+ {
559
+ "id": "inbox",
560
+ "screenId": "himi_owner_lite_inbox",
561
+ "label": "Inbox",
562
+ "icon": "chat"
563
+ },
535
564
  {
536
565
  "id": "aria",
537
566
  "screenId": "himi_owner_lite_aria",