@wix/himalaya-cli 0.1599.0 → 0.1600.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 (74) hide show
  1. package/dist/cli.mjs +1351 -977
  2. package/guides/himi-json-output.md +25 -1
  3. package/guides/one-app-to-himi-app.md +33 -11
  4. package/package.json +2 -2
  5. package/templates/beauty-salon/dev/index.ts +182 -0
  6. package/templates/beauty-salon/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  7. package/templates/beauty-salon/tier5-src/himi-blog-feed/index.ts +3 -1
  8. package/templates/beauty-salon/tier5-src/himi-blog-post/index.ts +42 -0
  9. package/templates/beauty-salon/tier5-src/himi-blog-saved/index.ts +4 -1
  10. package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +5 -2
  11. package/templates/branded-lite/dev/index.ts +182 -0
  12. package/templates/branded-lite/l10n/en.json +10 -7
  13. package/templates/branded-lite/tier5-src/_shared/header-badges.ts +113 -0
  14. package/templates/branded-lite/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  15. package/templates/branded-lite/tier5-src/_shared/source-badges.ts +70 -0
  16. package/templates/branded-lite/tier5-src/himi-blog-feed/index.ts +3 -1
  17. package/templates/branded-lite/tier5-src/himi-blog-post/index.ts +42 -0
  18. package/templates/branded-lite/tier5-src/himi-blog-saved/index.ts +4 -1
  19. package/templates/branded-lite/tier5-src/himi-branded-lite-header-badges/index.ts +38 -0
  20. package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +5 -2
  21. package/templates/community-club/dev/index.ts +182 -0
  22. package/templates/community-club/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  23. package/templates/community-club/tier5-src/himi-blog-feed/index.ts +3 -1
  24. package/templates/community-club/tier5-src/himi-blog-post/index.ts +42 -0
  25. package/templates/community-club/tier5-src/himi-blog-saved/index.ts +4 -1
  26. package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +5 -2
  27. package/templates/events-meetups/dev/index.ts +182 -0
  28. package/templates/events-meetups/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  29. package/templates/events-meetups/tier5-src/himi-blog-feed/index.ts +3 -1
  30. package/templates/events-meetups/tier5-src/himi-blog-post/index.ts +42 -0
  31. package/templates/events-meetups/tier5-src/himi-blog-saved/index.ts +4 -1
  32. package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +5 -2
  33. package/templates/gym-fitness/dev/index.ts +182 -0
  34. package/templates/gym-fitness/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  35. package/templates/gym-fitness/tier5-src/himi-blog-feed/index.ts +3 -1
  36. package/templates/gym-fitness/tier5-src/himi-blog-post/index.ts +42 -0
  37. package/templates/gym-fitness/tier5-src/himi-blog-saved/index.ts +4 -1
  38. package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +5 -2
  39. package/templates/nonprofit-cause/dev/index.ts +182 -0
  40. package/templates/nonprofit-cause/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  41. package/templates/nonprofit-cause/tier5-src/himi-blog-feed/index.ts +3 -1
  42. package/templates/nonprofit-cause/tier5-src/himi-blog-post/index.ts +42 -0
  43. package/templates/nonprofit-cause/tier5-src/himi-blog-saved/index.ts +4 -1
  44. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +5 -2
  45. package/templates/podcast-creator/dev/index.ts +182 -0
  46. package/templates/podcast-creator/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  47. package/templates/podcast-creator/tier5-src/himi-blog-feed/index.ts +3 -1
  48. package/templates/podcast-creator/tier5-src/himi-blog-post/index.ts +42 -0
  49. package/templates/podcast-creator/tier5-src/himi-blog-saved/index.ts +4 -1
  50. package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +5 -2
  51. package/templates/portfolio-creative/dev/index.ts +182 -0
  52. package/templates/portfolio-creative/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  53. package/templates/portfolio-creative/tier5-src/himi-blog-feed/index.ts +3 -1
  54. package/templates/portfolio-creative/tier5-src/himi-blog-post/index.ts +42 -0
  55. package/templates/portfolio-creative/tier5-src/himi-blog-saved/index.ts +4 -1
  56. package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +5 -2
  57. package/templates/restaurant/dev/index.ts +182 -0
  58. package/templates/restaurant/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  59. package/templates/restaurant/tier5-src/himi-blog-feed/index.ts +3 -1
  60. package/templates/restaurant/tier5-src/himi-blog-post/index.ts +42 -0
  61. package/templates/restaurant/tier5-src/himi-blog-saved/index.ts +4 -1
  62. package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +5 -2
  63. package/templates/retail-store/dev/index.ts +182 -0
  64. package/templates/retail-store/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  65. package/templates/retail-store/tier5-src/himi-blog-feed/index.ts +3 -1
  66. package/templates/retail-store/tier5-src/himi-blog-post/index.ts +42 -0
  67. package/templates/retail-store/tier5-src/himi-blog-saved/index.ts +4 -1
  68. package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +5 -2
  69. package/templates/yoga-studio/dev/index.ts +182 -0
  70. package/templates/yoga-studio/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  71. package/templates/yoga-studio/tier5-src/himi-blog-feed/index.ts +3 -1
  72. package/templates/yoga-studio/tier5-src/himi-blog-post/index.ts +42 -0
  73. package/templates/yoga-studio/tier5-src/himi-blog-saved/index.ts +4 -1
  74. package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +5 -2
@@ -406,7 +406,31 @@ unresolved migration finding.
406
406
  - `config_uninspectable`: the target `config.ts` cannot be read or the relevant
407
407
  `navigation.topBarDefault.actions` cannot be inspected (exit 1).
408
408
  - `topbar_coverage_failed`: a source top-bar control has no matching action
409
- with a non-empty `onTap.actionId` (exit 1).
409
+ with a non-empty `onTap.actionId`, or a source badge's action is not a complete
410
+ live count. When the app's `navLogicBundle` is a Branded Lite badge worker, the
411
+ badge's `valuePath` must also be a count that worker holds
412
+ (`$service:brandedLiteNav.chatBadge`, `.notificationsBadge`, `.cartBadge`) and sit on
413
+ the matching button (exit 1).
414
+
415
+ ## `himi one-app header-badges` codes
416
+
417
+ Binds the source header's bell, chat and cart counts in `config.ts`. Success prints
418
+ `ok: true` and a `status`: `written`, `unchanged`, `would_write` (`--dry-run`),
419
+ `no_badges` or `nothing_to_bind`; `unmapped` lists badge buttons that are not clearly
420
+ a bell, chat or cart button and are never guessed. Failures print `ok: false` and a
421
+ top-level `code` on stderr.
422
+
423
+ - `invalid_usage`: required options are missing, duplicated, or unknown (exit 2).
424
+ - `evidence_invalid`: the evidence manifest or a collected file is missing, invalid,
425
+ fails its recorded hash, or a platform's runtime context was unavailable (exit 1).
426
+ - `config_uninspectable`: `config.ts` or `himalaya.content.json` cannot be read, so
427
+ the app's screens and navigation cannot be inspected, or `config.ts` cannot be
428
+ written; in that case the coverage ledger is put back as it was (exit 1).
429
+ - `worker_missing`: the app has no `tier5-src/himi-branded-lite-header-badges`, so a
430
+ config naming that worker would 404; re-scaffold from the current template (exit 1).
431
+ - `config_needs_manual_merge`: `config.ts` already has a `topBarDefault` or
432
+ `navLogicBundle` that differs, or its `navigation` cannot be edited safely; the
433
+ literal to merge is returned in `snippet` and nothing is written (exit 1).
410
434
 
411
435
  ## `himi one-app validate-design` codes
412
436
 
@@ -533,17 +533,39 @@ Apply these interpretation rules:
533
533
  without observer/listener fields. Do not infer a count badge for every custom
534
534
  component. Inspect its implementation and retain unknown behavior as unresolved.
535
535
 
536
- Branded Lite's nav worker already counts the three source badges. Bind the bell to
537
- `$service:brandedLiteNav.notificationsBadge` (the member's unseen notifications, read for the site the
538
- native bridge is bound to), the chat button to `$service:brandedLiteNav.chatBadge` (the member's unread
539
- business chatrooms, as the server counts them, capped at 100) and the cart button to
540
- `$service:brandedLiteNav.cartBadge` (the shopper's cart item count, every line's quantity). Each is `null`
541
- while unavailable (a Visitor's bell and chat, offline, a failed or stale read, an old binary or the web
542
- for the bell) and the shell shows no badge for `null` or 0, so none is ever a fake zero. They refresh at
543
- launch, whenever the signed-in identity changes (the previous identity's numbers go first), on every
544
- foreground and on every notification receipt, and a read that finishes after the identity changed is
545
- dropped. They do not poll: a screen that changes the cart (an add-to-cart, a checkout) must call
546
- `tier5.brandedLiteNav.refreshBadges` itself, or the cart badge waits for the next trigger.
536
+ **Bell, chat and cart badges are bound by a command, not by hand.** `himi one-app migrate` writes
537
+ `header-badge-plan.json`: the counters the source header carries, found by each button's id, icon and
538
+ accessibility label (the observer or listener id only marks a button as carrying a badge; it does not
539
+ say which counter). A badge button that is not clearly a bell, chat or cart button is listed under
540
+ `unmapped`; its counter stays an open obligation, never guessed. After scaffolding from the Branded Lite
541
+ template and mapping each source screen to its Himi screen (the `route` rows of `ONE-APP-COVERAGE.json`), run
542
+
543
+ ```bash
544
+ himi one-app header-badges --evidence-dir .himi/one-app --content .
545
+ ```
546
+
547
+ It adds `navigation.topBarDefault` with one literal action per counter (ids `chat`, `notifications` and
548
+ `cart`, opening `himi_business_chat`, `himi_notifications` and `himi_cart`, each with
549
+ `badge: { kind: "count", valuePath: "$service:brandedLiteNav.<chatBadge|notificationsBadge|cartBadge>" }`)
550
+ and `navigation.navLogicBundle` for the badges-only worker the template already ships under
551
+ `tier5-src/himi-branded-lite-header-badges`, and it records each bound button's `target` in the coverage
552
+ ledger. A source with no badges changes nothing. It never overwrites: a config that already has a
553
+ `topBarDefault` or `navLogicBundle` (and a ledger target you set) is left alone and reported, with the
554
+ literal to merge (`config_needs_manual_merge`). Run it again after mapping routes if you ran it first:
555
+ the rows it wrote move to the mapped screen. Then run `validate-topbar`, which checks that each badge's
556
+ `valuePath` is a count the worker holds and sits on the right button. A screen with its own `topBar`
557
+ replaces `topBarDefault`, so `validate-topbar` still asks that screen to carry the action.
558
+
559
+ The counts: the bell is the member's unseen notifications (read for the site the native bridge is bound
560
+ to), chat is the member's unread business chatrooms (as the server counts them, capped at 100) and cart is
561
+ the shopper's item count (every line's quantity). Each is `null` while unavailable (a Visitor's bell and
562
+ chat, offline, a failed or stale read, an old binary or the web for the bell) and the shell shows no badge
563
+ for `null` or 0, so none is ever a fake zero. If the source shows the bell or chat badge to Visitors, the plan
564
+ warns: a Himi header shows them to signed-in members only. They refresh at launch, whenever the signed-in
565
+ identity changes (the previous identity's numbers go first), on every foreground and on every notification
566
+ receipt, and a read that finishes after the identity changed is dropped. They do not poll: a screen that
567
+ changes the cart (an add-to-cart, a checkout) must call `tier5.brandedLiteNav.refreshBadges` itself, or the
568
+ cart badge waits for the next trigger.
547
569
 
548
570
  Use a live `badge: { kind: "count", valuePath: "$service:sourceBadges.count" }`
549
571
  only after implementing the actual source counter. Preserve the source button
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1599.0",
3
+ "version": "0.1600.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -72,5 +72,5 @@
72
72
  "artifactId": "himalaya-cli"
73
73
  }
74
74
  },
75
- "falconPackageHash": "0492bb475381b374bdbf2ac75086d487a090cf829cbbf6f254c6ddd1"
75
+ "falconPackageHash": "345b024a67019de4aeafb2b81fa91e65d43675b03bb58533eefb85a7"
76
76
  }
@@ -42270,6 +42270,72 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
42270
42270
  }
42271
42271
  }
42272
42272
  }
42273
+ },
42274
+ {
42275
+ "id": "",
42276
+ "body": {
42277
+ "package": "himalaya",
42278
+ "type": "HStack",
42279
+ "props": {
42280
+ "spacing": 8,
42281
+ "children": [
42282
+ {
42283
+ "id": "himi_blog_post.report",
42284
+ "body": {
42285
+ "package": "himalaya",
42286
+ "type": "Button",
42287
+ "props": {
42288
+ "title": "Report",
42289
+ "style": "secondary",
42290
+ "fullWidth": true,
42291
+ "action": {
42292
+ "actionId": "tier5.himiBlogPost.report"
42293
+ }
42294
+ }
42295
+ },
42296
+ "modifiers": {
42297
+ "hiddenPath": "$service:himiBlogPost.reported"
42298
+ }
42299
+ },
42300
+ {
42301
+ "id": "himi_blog_post.hide",
42302
+ "body": {
42303
+ "package": "himalaya",
42304
+ "type": "Button",
42305
+ "props": {
42306
+ "title": "Hide post",
42307
+ "style": "secondary",
42308
+ "fullWidth": true,
42309
+ "action": {
42310
+ "actionId": "tier5.himiBlogPost.openHide"
42311
+ }
42312
+ }
42313
+ },
42314
+ "modifiers": {
42315
+ "hiddenPath": "$service:himiBlogPost.hidden"
42316
+ }
42317
+ },
42318
+ {
42319
+ "id": "himi_blog_post.unhide",
42320
+ "body": {
42321
+ "package": "himalaya",
42322
+ "type": "Button",
42323
+ "props": {
42324
+ "title": "Undo hide",
42325
+ "style": "secondary",
42326
+ "fullWidth": true,
42327
+ "action": {
42328
+ "actionId": "tier5.himiBlogPost.unhide"
42329
+ }
42330
+ }
42331
+ },
42332
+ "modifiers": {
42333
+ "hiddenPath": "$service:himiBlogPost.notHidden"
42334
+ }
42335
+ }
42336
+ ]
42337
+ }
42338
+ }
42273
42339
  }
42274
42340
  ]
42275
42341
  }
@@ -42528,6 +42594,122 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
42528
42594
  "hiddenPath": "$service:himiBlogPost.noPost"
42529
42595
  }
42530
42596
  },
42597
+ {
42598
+ "id": "himi_blog_post.hide_sheet",
42599
+ "body": {
42600
+ "package": "himalaya",
42601
+ "type": "BottomSheet",
42602
+ "props": {
42603
+ "presentedPath": "$service:himiBlogPost.hideSheetOpen",
42604
+ "dismissible": true,
42605
+ "showGrabber": true,
42606
+ "onDismiss": {
42607
+ "actionId": "tier5.himiBlogPost.closeHide"
42608
+ },
42609
+ "content": {
42610
+ "id": "",
42611
+ "body": {
42612
+ "package": "himalaya",
42613
+ "type": "VStack",
42614
+ "props": {
42615
+ "spacing": 12,
42616
+ "alignment": "leading",
42617
+ "children": [
42618
+ {
42619
+ "id": "",
42620
+ "body": {
42621
+ "package": "himalaya",
42622
+ "type": "Text",
42623
+ "props": {
42624
+ "text": "Why are you hiding this post?",
42625
+ "style": "title3"
42626
+ }
42627
+ }
42628
+ },
42629
+ {
42630
+ "id": "himi_blog_post.hide_offensive",
42631
+ "body": {
42632
+ "package": "himalaya",
42633
+ "type": "Button",
42634
+ "props": {
42635
+ "title": "It is offensive",
42636
+ "style": "secondary",
42637
+ "fullWidth": true,
42638
+ "action": {
42639
+ "actionId": "tier5.himiBlogPost.hide",
42640
+ "params": {
42641
+ "reason": "offensive"
42642
+ }
42643
+ }
42644
+ }
42645
+ }
42646
+ },
42647
+ {
42648
+ "id": "himi_blog_post.hide_misleading",
42649
+ "body": {
42650
+ "package": "himalaya",
42651
+ "type": "Button",
42652
+ "props": {
42653
+ "title": "It is misleading",
42654
+ "style": "secondary",
42655
+ "fullWidth": true,
42656
+ "action": {
42657
+ "actionId": "tier5.himiBlogPost.hide",
42658
+ "params": {
42659
+ "reason": "misleading"
42660
+ }
42661
+ }
42662
+ }
42663
+ }
42664
+ },
42665
+ {
42666
+ "id": "himi_blog_post.hide_other",
42667
+ "body": {
42668
+ "package": "himalaya",
42669
+ "type": "Button",
42670
+ "props": {
42671
+ "title": "Something else",
42672
+ "style": "secondary",
42673
+ "fullWidth": true,
42674
+ "action": {
42675
+ "actionId": "tier5.himiBlogPost.hide",
42676
+ "params": {
42677
+ "reason": "other"
42678
+ }
42679
+ }
42680
+ }
42681
+ }
42682
+ },
42683
+ {
42684
+ "id": "himi_blog_post.hide_cancel",
42685
+ "body": {
42686
+ "package": "himalaya",
42687
+ "type": "Button",
42688
+ "props": {
42689
+ "title": "Cancel",
42690
+ "style": "secondary",
42691
+ "fullWidth": true,
42692
+ "action": {
42693
+ "actionId": "tier5.himiBlogPost.closeHide"
42694
+ }
42695
+ }
42696
+ }
42697
+ }
42698
+ ]
42699
+ }
42700
+ },
42701
+ "modifiers": {
42702
+ "padding": {
42703
+ "leading": 20,
42704
+ "trailing": 20,
42705
+ "top": 16,
42706
+ "bottom": 24
42707
+ }
42708
+ }
42709
+ }
42710
+ }
42711
+ }
42712
+ },
42531
42713
  {
42532
42714
  "id": "",
42533
42715
  "body": {
@@ -0,0 +1,66 @@
1
+ // The posts this device's reader chose to hide, as the One App blog does: a client-side list of post ids that the
2
+ // blog feed, the Home cards and Saved leave out. Nothing is sent to the site, and another device or account does not
3
+ // share it. It is the saved-posts store's twin (see himi-blog-saved.ts) and reads storage the same way: native workers
4
+ // have only the sync `himalaya.storage` KV, a browser has `localStorage`, and each screen runs its own worker, so
5
+ // the durable arm is what lets a Hide on the post screen reach the feed's worker.
6
+ const KEY = "himi-blog:hidden-posts:v1";
7
+
8
+ type StorageLike = {
9
+ getItem: (key: string) => string | null;
10
+ setItem: (key: string, value: string) => void;
11
+ };
12
+
13
+ type HiddenHost = typeof globalThis & {
14
+ himalaya?: { storage?: StorageLike };
15
+ localStorage?: StorageLike;
16
+ __himiBlogHiddenIds?: string[];
17
+ };
18
+
19
+ const host = globalThis as HiddenHost;
20
+
21
+ function storage(): StorageLike | undefined {
22
+ return host.himalaya?.storage ?? host.localStorage;
23
+ }
24
+
25
+ const unique = (ids: string[]): string[] => Array.from(new Set(ids.map((id) => id.trim()).filter(Boolean)));
26
+
27
+ export function readHiddenIds(): string[] {
28
+ try {
29
+ const raw = storage()?.getItem(KEY);
30
+ if (raw) {
31
+ const parsed = JSON.parse(raw);
32
+ if (Array.isArray(parsed)) return unique(parsed.filter((id): id is string => typeof id === "string"));
33
+ }
34
+ } catch {
35
+ // Unreadable storage: fall back to what this worker context holds in memory.
36
+ }
37
+ return unique(host.__himiBlogHiddenIds ?? []);
38
+ }
39
+
40
+ function writeHiddenIds(ids: string[]): void {
41
+ const next = unique(ids);
42
+ host.__himiBlogHiddenIds = next;
43
+ try {
44
+ storage()?.setItem(KEY, JSON.stringify(next));
45
+ } catch {
46
+ // Storage-less hosts keep the in-memory arm only.
47
+ }
48
+ }
49
+
50
+ export function isHiddenId(id: string): boolean {
51
+ return readHiddenIds().includes(id);
52
+ }
53
+
54
+ export function hideId(id: string): void {
55
+ writeHiddenIds([id, ...readHiddenIds()]);
56
+ }
57
+
58
+ export function unhideId(id: string): void {
59
+ writeHiddenIds(readHiddenIds().filter((hiddenId) => hiddenId !== id));
60
+ }
61
+
62
+ /** The items a list may show: every one whose `id` this device has not hidden. */
63
+ export function withoutHidden<T extends { id?: string | null }>(items: T[]): T[] {
64
+ const hidden = new Set(readHiddenIds());
65
+ return hidden.size ? items.filter((item) => !item.id || !hidden.has(item.id)) : items;
66
+ }
@@ -4,6 +4,7 @@ import { queryCategories, queryPosts, type WixPost } from "@himalaya/wix-blog";
4
4
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
5
5
  import { getAll, upsert, type DbEntity } from "@himalaya/local-db";
6
6
  import { formatDateMedium } from "../_shared/engine-intl.js";
7
+ import { withoutHidden } from "../_shared/himi-blog-hidden.js";
7
8
 
8
9
  const PAGE_SIZE = 20;
9
10
 
@@ -137,7 +138,8 @@ function normalize(post: WixPost, categoryMap: Map<string, string>): PostRow {
137
138
 
138
139
  function applyFilters(s: S): S {
139
140
  const needle = s.query.trim().toLowerCase();
140
- const posts = s.allPosts.filter((post) => {
141
+ // Posts this device's reader hid (on the post screen) never show, whatever the cache or the live page holds.
142
+ const posts = withoutHidden(s.allPosts).filter((post) => {
141
143
  const categoryOk = !s.selectedCategoryId || post.categoryId === s.selectedCategoryId;
142
144
  const queryOk =
143
145
  !needle ||
@@ -5,6 +5,7 @@ import { getPost, likePost, unlikePost, type WixPost, type WixPostWithContent }
5
5
  import { comments, BLOG_COMMENTS_APP_ID, blogCommentRow, listBlogComments, type WixBlogCommentRow } from "@himalaya/wix-comments";
6
6
  import { get, type DbEntity } from "@himalaya/local-db";
7
7
  import { isSavedId, toggleSavedId } from "../_shared/himi-blog-saved";
8
+ import { hideId, isHiddenId, unhideId } from "../_shared/himi-blog-hidden";
8
9
  import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
9
10
  import { formatDateMedium, formatMonthDay } from "../_shared/engine-intl.js";
10
11
  import { openRichLink } from "../_shared/rich-content-link.js";
@@ -64,6 +65,14 @@ interface S {
64
65
  notLiked: boolean;
65
66
  saved: boolean;
66
67
  notSaved: boolean;
68
+ /** This device's reader hid the post: the feed, Home and Saved leave it out, and this screen offers Undo. */
69
+ hidden: boolean;
70
+ notHidden: boolean;
71
+ /** The reason sheet (offensive / misleading / other) is open. */
72
+ hideSheetOpen: boolean;
73
+ /** The reader reported the post this visit; the report button then says so and stops sending. */
74
+ reported: boolean;
75
+ notReported: boolean;
67
76
  commentDraft: string;
68
77
  comments: CommentRow[];
69
78
  commentsUnavailable: boolean;
@@ -212,6 +221,8 @@ function derive(s: S): S {
212
221
  hasBodyJson: s.bodyJson !== "",
213
222
  notLiked: !s.liked,
214
223
  notSaved: !s.saved,
224
+ notHidden: !s.hidden,
225
+ notReported: !s.reported,
215
226
  commentsAvailable: !s.commentsUnavailable,
216
227
  };
217
228
  }
@@ -238,6 +249,11 @@ const baseState: S = derive({
238
249
  notLiked: true,
239
250
  saved: false,
240
251
  notSaved: true,
252
+ hidden: false,
253
+ notHidden: true,
254
+ hideSheetOpen: false,
255
+ reported: false,
256
+ notReported: true,
241
257
  commentDraft: "",
242
258
  comments: [],
243
259
  commentsUnavailable: false,
@@ -278,6 +294,7 @@ async function cachedPostState(s: S, id: string): Promise<S> {
278
294
  initial: String(cached.initial ?? "") || (title[0] || "#").toUpperCase(),
279
295
  liked: false,
280
296
  saved: isSavedId(id),
297
+ hidden: isHiddenId(id),
281
298
  comments: [],
282
299
  commentsUnavailable: true,
283
300
  commentStatusText: copyParam("blogPost.commentsUnavailable", "Comments are unavailable right now."),
@@ -331,6 +348,7 @@ async function loadPostState(s: S): Promise<S | undefined> {
331
348
  initial: (title[0] || "#").toUpperCase(),
332
349
  liked: Boolean(post.memberInteraction?.liked),
333
350
  saved: isSavedId(id),
351
+ hidden: isHiddenId(id),
334
352
  comments,
335
353
  commentsUnavailable,
336
354
  commentStatusText: commentsUnavailable
@@ -359,6 +377,14 @@ const svc = defineService<S, {
359
377
  like: (s: S) => Promise<S>;
360
378
  unlike: (s: S) => Promise<S>;
361
379
  toggleSave: (s: S) => S;
380
+ /** Report the post: no site API takes a member's report, so the reader is thanked and the tap is the signal. */
381
+ report: (s: S) => S;
382
+ openHide: (s: S) => S;
383
+ closeHide: (s: S) => S;
384
+ /** Hide the post on this device. The sheet's three reasons (offensive, misleading, other) are asked for as the One
385
+ * App asks them, but the list keeps only the id and the site is told nothing, so the worker does not read the reason. */
386
+ hide: (s: S) => S;
387
+ unhide: (s: S) => S;
362
388
  share: (s: S) => Promise<S>;
363
389
  setComment: (s: S, arg?: unknown) => S;
364
390
  submitComment: (s: S) => Promise<S>;
@@ -397,6 +423,22 @@ const svc = defineService<S, {
397
423
  const saved = toggleSavedId(s.id);
398
424
  return derive({ ...s, saved, actionMessage: saved ? copyParam("blogPost.savedMessage", "Saved for later") : copyParam("blogPost.unsavedMessage", "Removed from saved") });
399
425
  },
426
+ report: (s) => {
427
+ if (!s.id || s.reported) return s;
428
+ return derive({ ...s, reported: true, actionMessage: copyParam("blogPost.reportedMessage", "Post reported") });
429
+ },
430
+ openHide: (s) => s.id ? derive({ ...s, hideSheetOpen: true }) : s,
431
+ closeHide: (s) => derive({ ...s, hideSheetOpen: false }),
432
+ hide: (s) => {
433
+ if (!s.id) return s;
434
+ hideId(s.id);
435
+ return derive({ ...s, hidden: true, hideSheetOpen: false, actionMessage: copyParam("blogPost.hiddenMessage", "Post hidden. It will not show in your feeds.") });
436
+ },
437
+ unhide: (s) => {
438
+ if (!s.id) return s;
439
+ unhideId(s.id);
440
+ return derive({ ...s, hidden: false, actionMessage: copyParam("blogPost.unhiddenMessage", "Post is visible again") });
441
+ },
400
442
  share: async (s) => {
401
443
  if (!s.title) return s;
402
444
  try {
@@ -3,6 +3,7 @@ import { defineService } from "@himalaya/state";
3
3
  import { getPost, type WixPost } from "@himalaya/wix-blog";
4
4
  import { get, type DbEntity } from "@himalaya/local-db";
5
5
  import { readSavedIds, writeSavedIds } from "../_shared/himi-blog-saved";
6
+ import { readHiddenIds } from "../_shared/himi-blog-hidden";
6
7
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
7
8
  import { formatDateMedium } from "../_shared/engine-intl.js";
8
9
 
@@ -144,7 +145,9 @@ const baseState: S = derive({
144
145
  });
145
146
 
146
147
  async function loadSavedState(s: S): Promise<S> {
147
- const ids = readSavedIds();
148
+ // A saved post this device's reader also hid stays in the saved list (Undo brings it back) but is not shown.
149
+ const hidden = new Set(readHiddenIds());
150
+ const ids = readSavedIds().filter((id) => !hidden.has(id));
148
151
  if (!ids.length) {
149
152
  return derive({ ...s, status: "ready", posts: [], emptyText: copyParam("saved.emptyText", "No saved posts yet.") });
150
153
  }
@@ -26,6 +26,7 @@ import { formatDateMedium } from "../_shared/engine-intl.js";
26
26
  import { readSavedProducts, type SavedProduct } from "../_shared/himi-store-saved.js";
27
27
  import { loadStoreCategories, type StoreCategoryCard } from "../_shared/himi-store-categories.js";
28
28
  import { readSavedIds } from "../_shared/himi-blog-saved.js";
29
+ import { withoutHidden } from "../_shared/himi-blog-hidden.js";
29
30
  import { wixMediaUrl } from "@himalaya/wix-media";
30
31
  import { groupImage, programImage } from "../_shared/himi-media.js";
31
32
  import { isWixVideoWebRefusal, listLatestVideos, videoCard, type WixVideoCard } from "@himalaya/wix-video";
@@ -770,8 +771,10 @@ async function fetchExtrasSignal(arg: unknown): Promise<ExtrasSignal> {
770
771
  // read used to make; a failure collapses both blocks silently.
771
772
  jobs.push(queryPosts({ query: { paging: { limit: 8 }, sort: [{ fieldName: "firstPublishedDate", order: "DESC" }] } })
772
773
  .then((response) => {
773
- extras.announcementTitle = cleanTitle(response.posts?.[0]?.title);
774
- extras.postCards = (response.posts ?? []).filter((post) => post.id).slice(0, 6).map(postCard);
774
+ // Posts this device's reader hid on the post screen are left out of both blocks.
775
+ const posts = withoutHidden(response.posts ?? []);
776
+ extras.announcementTitle = cleanTitle(posts[0]?.title);
777
+ extras.postCards = posts.filter((post) => post.id).slice(0, 6).map(postCard);
775
778
  })
776
779
  .catch(() => { /* collapse silently */ }));
777
780
  }