@wix/himalaya-cli 0.1598.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 (98) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/legacy-import/src/index.d.ts +30 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/sdk-types/src/capabilities.generated.d.ts +2 -1
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/webview-page/src/capabilities.generated.d.ts +2 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/define-app.d.ts +12 -1
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/legacy-import.d.ts +134 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/audio-record.mjs +1 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calendar.mjs +1 -0
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calls.mjs +1 -0
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/contacts.mjs +1 -0
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +184 -16
  11. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/legacy-import.mjs +40 -0
  12. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/location.mjs +1 -0
  13. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/motion.mjs +1 -0
  14. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/photos.mjs +1 -0
  15. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdk-types.mjs +1 -0
  16. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/tasks.mjs +1 -0
  17. package/dist/authoring-runtime/node_modules/@wix/himalaya/package.json +4 -0
  18. package/dist/cli.mjs +1421 -1015
  19. package/dist/preview-assets.generated.bin +0 -0
  20. package/guides/app-config.md +1 -1
  21. package/guides/capabilities.md +1 -0
  22. package/guides/himi-json-output.md +25 -1
  23. package/guides/importable-sdks.md +1 -0
  24. package/guides/one-app-to-himi-app.md +33 -11
  25. package/guides/sdk-reference.json +43 -0
  26. package/guides/web-page-native-sdk.md +1 -1
  27. package/guides/web-page-sdk-reference.md +1 -0
  28. package/package.json +2 -2
  29. package/templates/beauty-salon/dev/index.ts +182 -0
  30. package/templates/beauty-salon/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  31. package/templates/beauty-salon/tier5-src/himi-blog-feed/index.ts +3 -1
  32. package/templates/beauty-salon/tier5-src/himi-blog-post/index.ts +42 -0
  33. package/templates/beauty-salon/tier5-src/himi-blog-saved/index.ts +4 -1
  34. package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +5 -2
  35. package/templates/branded-lite/dev/index.ts +182 -0
  36. package/templates/branded-lite/l10n/en.json +10 -7
  37. package/templates/branded-lite/tier5-src/_shared/header-badges.ts +113 -0
  38. package/templates/branded-lite/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  39. package/templates/branded-lite/tier5-src/_shared/source-badges.ts +70 -0
  40. package/templates/branded-lite/tier5-src/himi-blog-feed/index.ts +3 -1
  41. package/templates/branded-lite/tier5-src/himi-blog-post/index.ts +42 -0
  42. package/templates/branded-lite/tier5-src/himi-blog-saved/index.ts +4 -1
  43. package/templates/branded-lite/tier5-src/himi-branded-lite-header-badges/index.ts +38 -0
  44. package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +5 -2
  45. package/templates/community-club/dev/index.ts +182 -0
  46. package/templates/community-club/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  47. package/templates/community-club/tier5-src/himi-blog-feed/index.ts +3 -1
  48. package/templates/community-club/tier5-src/himi-blog-post/index.ts +42 -0
  49. package/templates/community-club/tier5-src/himi-blog-saved/index.ts +4 -1
  50. package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +5 -2
  51. package/templates/events-meetups/dev/index.ts +182 -0
  52. package/templates/events-meetups/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  53. package/templates/events-meetups/tier5-src/himi-blog-feed/index.ts +3 -1
  54. package/templates/events-meetups/tier5-src/himi-blog-post/index.ts +42 -0
  55. package/templates/events-meetups/tier5-src/himi-blog-saved/index.ts +4 -1
  56. package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +5 -2
  57. package/templates/gym-fitness/dev/index.ts +182 -0
  58. package/templates/gym-fitness/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  59. package/templates/gym-fitness/tier5-src/himi-blog-feed/index.ts +3 -1
  60. package/templates/gym-fitness/tier5-src/himi-blog-post/index.ts +42 -0
  61. package/templates/gym-fitness/tier5-src/himi-blog-saved/index.ts +4 -1
  62. package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +5 -2
  63. package/templates/nonprofit-cause/dev/index.ts +182 -0
  64. package/templates/nonprofit-cause/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  65. package/templates/nonprofit-cause/tier5-src/himi-blog-feed/index.ts +3 -1
  66. package/templates/nonprofit-cause/tier5-src/himi-blog-post/index.ts +42 -0
  67. package/templates/nonprofit-cause/tier5-src/himi-blog-saved/index.ts +4 -1
  68. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +5 -2
  69. package/templates/podcast-creator/dev/index.ts +182 -0
  70. package/templates/podcast-creator/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  71. package/templates/podcast-creator/tier5-src/himi-blog-feed/index.ts +3 -1
  72. package/templates/podcast-creator/tier5-src/himi-blog-post/index.ts +42 -0
  73. package/templates/podcast-creator/tier5-src/himi-blog-saved/index.ts +4 -1
  74. package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +5 -2
  75. package/templates/portfolio-creative/dev/index.ts +182 -0
  76. package/templates/portfolio-creative/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  77. package/templates/portfolio-creative/tier5-src/himi-blog-feed/index.ts +3 -1
  78. package/templates/portfolio-creative/tier5-src/himi-blog-post/index.ts +42 -0
  79. package/templates/portfolio-creative/tier5-src/himi-blog-saved/index.ts +4 -1
  80. package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +5 -2
  81. package/templates/restaurant/dev/index.ts +182 -0
  82. package/templates/restaurant/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  83. package/templates/restaurant/tier5-src/himi-blog-feed/index.ts +3 -1
  84. package/templates/restaurant/tier5-src/himi-blog-post/index.ts +42 -0
  85. package/templates/restaurant/tier5-src/himi-blog-saved/index.ts +4 -1
  86. package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +5 -2
  87. package/templates/retail-store/dev/index.ts +182 -0
  88. package/templates/retail-store/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  89. package/templates/retail-store/tier5-src/himi-blog-feed/index.ts +3 -1
  90. package/templates/retail-store/tier5-src/himi-blog-post/index.ts +42 -0
  91. package/templates/retail-store/tier5-src/himi-blog-saved/index.ts +4 -1
  92. package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +5 -2
  93. package/templates/yoga-studio/dev/index.ts +182 -0
  94. package/templates/yoga-studio/tier5-src/_shared/himi-blog-hidden.ts +66 -0
  95. package/templates/yoga-studio/tier5-src/himi-blog-feed/index.ts +3 -1
  96. package/templates/yoga-studio/tier5-src/himi-blog-post/index.ts +42 -0
  97. package/templates/yoga-studio/tier5-src/himi-blog-saved/index.ts +4 -1
  98. package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +5 -2
@@ -107652,6 +107652,72 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
107652
107652
  }
107653
107653
  }
107654
107654
  }
107655
+ },
107656
+ {
107657
+ "id": "",
107658
+ "body": {
107659
+ "package": "himalaya",
107660
+ "type": "HStack",
107661
+ "props": {
107662
+ "spacing": 8,
107663
+ "children": [
107664
+ {
107665
+ "id": "himi_blog_post.report",
107666
+ "body": {
107667
+ "package": "himalaya",
107668
+ "type": "Button",
107669
+ "props": {
107670
+ "title": "Report",
107671
+ "style": "secondary",
107672
+ "fullWidth": true,
107673
+ "action": {
107674
+ "actionId": "tier5.himiBlogPost.report"
107675
+ }
107676
+ }
107677
+ },
107678
+ "modifiers": {
107679
+ "hiddenPath": "$service:himiBlogPost.reported"
107680
+ }
107681
+ },
107682
+ {
107683
+ "id": "himi_blog_post.hide",
107684
+ "body": {
107685
+ "package": "himalaya",
107686
+ "type": "Button",
107687
+ "props": {
107688
+ "title": "Hide post",
107689
+ "style": "secondary",
107690
+ "fullWidth": true,
107691
+ "action": {
107692
+ "actionId": "tier5.himiBlogPost.openHide"
107693
+ }
107694
+ }
107695
+ },
107696
+ "modifiers": {
107697
+ "hiddenPath": "$service:himiBlogPost.hidden"
107698
+ }
107699
+ },
107700
+ {
107701
+ "id": "himi_blog_post.unhide",
107702
+ "body": {
107703
+ "package": "himalaya",
107704
+ "type": "Button",
107705
+ "props": {
107706
+ "title": "Undo hide",
107707
+ "style": "secondary",
107708
+ "fullWidth": true,
107709
+ "action": {
107710
+ "actionId": "tier5.himiBlogPost.unhide"
107711
+ }
107712
+ }
107713
+ },
107714
+ "modifiers": {
107715
+ "hiddenPath": "$service:himiBlogPost.notHidden"
107716
+ }
107717
+ }
107718
+ ]
107719
+ }
107720
+ }
107655
107721
  }
107656
107722
  ]
107657
107723
  }
@@ -107910,6 +107976,122 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
107910
107976
  "hiddenPath": "$service:himiBlogPost.noPost"
107911
107977
  }
107912
107978
  },
107979
+ {
107980
+ "id": "himi_blog_post.hide_sheet",
107981
+ "body": {
107982
+ "package": "himalaya",
107983
+ "type": "BottomSheet",
107984
+ "props": {
107985
+ "presentedPath": "$service:himiBlogPost.hideSheetOpen",
107986
+ "dismissible": true,
107987
+ "showGrabber": true,
107988
+ "onDismiss": {
107989
+ "actionId": "tier5.himiBlogPost.closeHide"
107990
+ },
107991
+ "content": {
107992
+ "id": "",
107993
+ "body": {
107994
+ "package": "himalaya",
107995
+ "type": "VStack",
107996
+ "props": {
107997
+ "spacing": 12,
107998
+ "alignment": "leading",
107999
+ "children": [
108000
+ {
108001
+ "id": "",
108002
+ "body": {
108003
+ "package": "himalaya",
108004
+ "type": "Text",
108005
+ "props": {
108006
+ "text": "Why are you hiding this post?",
108007
+ "style": "title3"
108008
+ }
108009
+ }
108010
+ },
108011
+ {
108012
+ "id": "himi_blog_post.hide_offensive",
108013
+ "body": {
108014
+ "package": "himalaya",
108015
+ "type": "Button",
108016
+ "props": {
108017
+ "title": "It is offensive",
108018
+ "style": "secondary",
108019
+ "fullWidth": true,
108020
+ "action": {
108021
+ "actionId": "tier5.himiBlogPost.hide",
108022
+ "params": {
108023
+ "reason": "offensive"
108024
+ }
108025
+ }
108026
+ }
108027
+ }
108028
+ },
108029
+ {
108030
+ "id": "himi_blog_post.hide_misleading",
108031
+ "body": {
108032
+ "package": "himalaya",
108033
+ "type": "Button",
108034
+ "props": {
108035
+ "title": "It is misleading",
108036
+ "style": "secondary",
108037
+ "fullWidth": true,
108038
+ "action": {
108039
+ "actionId": "tier5.himiBlogPost.hide",
108040
+ "params": {
108041
+ "reason": "misleading"
108042
+ }
108043
+ }
108044
+ }
108045
+ }
108046
+ },
108047
+ {
108048
+ "id": "himi_blog_post.hide_other",
108049
+ "body": {
108050
+ "package": "himalaya",
108051
+ "type": "Button",
108052
+ "props": {
108053
+ "title": "Something else",
108054
+ "style": "secondary",
108055
+ "fullWidth": true,
108056
+ "action": {
108057
+ "actionId": "tier5.himiBlogPost.hide",
108058
+ "params": {
108059
+ "reason": "other"
108060
+ }
108061
+ }
108062
+ }
108063
+ }
108064
+ },
108065
+ {
108066
+ "id": "himi_blog_post.hide_cancel",
108067
+ "body": {
108068
+ "package": "himalaya",
108069
+ "type": "Button",
108070
+ "props": {
108071
+ "title": "Cancel",
108072
+ "style": "secondary",
108073
+ "fullWidth": true,
108074
+ "action": {
108075
+ "actionId": "tier5.himiBlogPost.closeHide"
108076
+ }
108077
+ }
108078
+ }
108079
+ }
108080
+ ]
108081
+ }
108082
+ },
108083
+ "modifiers": {
108084
+ "padding": {
108085
+ "leading": 20,
108086
+ "trailing": 20,
108087
+ "top": 16,
108088
+ "bottom": 24
108089
+ }
108090
+ }
108091
+ }
108092
+ }
108093
+ }
108094
+ },
107913
108095
  {
107914
108096
  "id": "",
107915
108097
  "body": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$source": "en",
3
- "$generated": "2026-11-06T04:07:12.242Z",
3
+ "$generated": "2026-11-07T14:00:37.597Z",
4
4
  "messages": {
5
5
  "h_042c862e4467": {
6
6
  "source": "Try again",
@@ -8,6 +8,7 @@
8
8
  "occurrences": [
9
9
  "himi_branded_home#payload.body.props.children[0].body.props.children[1].body.props.title",
10
10
  "himi_account_settings#payload.body.props.children[3].body.props.children[1].body.props.title",
11
+ "himi_notif_settings#payload.body.props.children[7].body.props.children[1].body.props.title",
11
12
  "himi_shop#payload.body.props.children[6].body.props.children[3].body.props.title",
12
13
  "himi_saved_products#payload.body.props.children[3].body.props.children[2].body.props.title",
13
14
  "himi_collections#payload.body.props.children[4].body.props.ctaLabel",
@@ -26,22 +27,23 @@
26
27
  "himi_member_programs#payload.body.props.children[5].body.props.children[2].body.props.title",
27
28
  "himi_plans#payload.body.props.children[4].body.props.children[2].body.props.title",
28
29
  "himi_feed#payload.body.props.children[8].body.props.children[2].body.props.title",
29
- "himi_members#payload.body.props.children[4].body.props.children[1].body.props.title",
30
+ "himi_members#payload.body.props.children[5].body.props.children[1].body.props.title",
30
31
  "himi_coupons#payload.body.props.children[4].body.props.children[8].body.props.children[2].body.props.title",
31
32
  "himi_notifications#payload.body.props.children[6].body.props.children[8].body.props.children[2].body.props.title",
32
- "himi_loyalty#payload.body.props.children[4].body.props.children[8].body.props.children[2].body.props.title",
33
+ "himi_loyalty#payload.body.props.children[5].body.props.children[1].body.props.title",
33
34
  "himi_reviews#payload.body.props.children[4].body.props.children[8].body.props.children[2].body.props.title",
34
35
  "himi_portfolio#payload.body.props.children[2].body.props.children[8].body.props.children[2].body.props.title",
35
36
  "himi_portfolio_project#payload.body.props.children[1].body.props.children[1].body.props.title",
36
37
  "himi_reservations#payload.body.props.children[6].body.props.children[1].body.props.title",
37
38
  "himi_invoices#payload.body.props.children[4].body.props.children[8].body.props.children[2].body.props.title",
38
39
  "himi_badges#payload.body.props.children[4].body.props.children[8].body.props.children[2].body.props.title",
39
- "himi_notif_settings#payload.body.props.children[7].body.props.children[1].body.props.title",
40
40
  "himi_events#payload.body.props.children[2].body.props.children[2].body.props.title",
41
41
  "himi_my_events#payload.body.props.children[1].body.props.children[2].body.props.title",
42
42
  "himi_blog#payload.body.props.children[2].body.props.children[2].body.props.title",
43
43
  "himi_saved#payload.body.props.children[1].body.props.children[2].body.props.title",
44
- "himi_groups#payload.body.props.children[4].body.props.children[2].body.props.title",
44
+ "himi_groups#payload.body.props.children[5].body.props.children[2].body.props.title",
45
+ "himi_group_about#payload.body.props.children[2].body.props.children[1].body.props.title",
46
+ "himi_my_groups#payload.body.props.children[2].body.props.children[2].body.props.title",
45
47
  "himi_faq#payload.body.props.children[6].body.props.children[2].body.props.title",
46
48
  "himi_steps#payload.body.props.children[4].body.props.children[2].body.props.title",
47
49
  "himi_cms#payload.body.props.children[3].body.props.children[7].body.props.children[2].body.props.title",
@@ -397,6 +399,7 @@
397
399
  "kind": "Button.title",
398
400
  "occurrences": [
399
401
  "himi_account_settings#payload.body.props.children[3].body.props.children[2].body.props.title",
402
+ "himi_notif_settings#payload.body.props.children[7].body.props.children[2].body.props.title",
400
403
  "himi_view_profile#payload.body.props.children[2].body.props.children[3].body.props.title",
401
404
  "himi_orders#payload.body.props.children[11].body.props.children[3].body.props.title",
402
405
  "himi_addresses#payload.body.props.children[8].body.props.children[3].body.props.title",
@@ -409,16 +412,16 @@
409
412
  "himi_chat_thread#payload.body.props.children[3].body.props.title",
410
413
  "himi_coupons#payload.body.props.children[4].body.props.children[8].body.props.children[3].body.props.title",
411
414
  "himi_notifications#payload.body.props.children[6].body.props.children[8].body.props.children[3].body.props.title",
412
- "himi_loyalty#payload.body.props.children[4].body.props.children[8].body.props.children[3].body.props.title",
415
+ "himi_loyalty#payload.body.props.children[7].body.props.children[1].body.props.children[3].body.props.title",
413
416
  "himi_reviews#payload.body.props.children[4].body.props.children[8].body.props.children[3].body.props.title",
414
417
  "himi_portfolio#payload.body.props.children[2].body.props.children[8].body.props.children[3].body.props.title",
415
418
  "himi_reservations#payload.body.props.children[6].body.props.children[2].body.props.title",
416
419
  "himi_invoices#payload.body.props.children[4].body.props.children[8].body.props.children[3].body.props.title",
417
420
  "himi_badges#payload.body.props.children[4].body.props.children[8].body.props.children[3].body.props.title",
418
- "himi_notif_settings#payload.body.props.children[7].body.props.children[2].body.props.title",
419
421
  "himi_create_post#payload.body.props.children[1].body.props.children[3].body.props.title",
420
422
  "himi_forum_post#payload.body.props.children[3].body.props.title",
421
423
  "himi_my_events#payload.body.props.children[1].body.props.children[3].body.props.title",
424
+ "himi_my_groups#payload.body.props.children[6].body.props.children[3].body.props.title",
422
425
  "himi_cms#payload.body.props.children[3].body.props.children[7].body.props.children[3].body.props.title",
423
426
  "himi_donations#payload.body.props.children[4].body.props.children[8].body.props.children[3].body.props.title",
424
427
  "himi_gift_cards#payload.body.props.children[4].body.props.children[8].body.props.children[3].body.props.title",
@@ -0,0 +1,113 @@
1
+ // The header badge counts (bell, chat, cart) as one service piece, shared by the app-level nav workers that
2
+ // bind them: `himi-branded-lite-nav` (the full nav worker of the Branded Lite app) and `himi-branded-lite-header-badges`
3
+ // (the badges-only worker a scaffold ships). Both name their service `brandedLiteNav`, so a top-bar action binds the
4
+ // same `$service:brandedLiteNav.<field>` path in either.
5
+ //
6
+ // `null` is unavailable and never a zero (see source-badges.ts). The counts refresh at launch, whenever the
7
+ // signed-in identity changes (the native host session clock, or the reserved `onIdentityChange` hook a web host
8
+ // delivers), on every foreground (`onForeground`), and whenever the worker is asked to. A read that finishes after
9
+ // the identity changed, or after a newer read began, is dropped.
10
+ import { hostSessionFromGlobal } from "@himalaya/iap";
11
+ import { readSourceBadges } from "./source-badges.js";
12
+
13
+ export interface HeaderBadgeState {
14
+ notificationsBadge: number | null;
15
+ chatBadge: number | null;
16
+ cartBadge: number | null;
17
+ /** Which counts the last refresh tried and failed to read ("" when it read every count it could). A count that is
18
+ * unavailable by design (a Visitor's bell and chat, offline) is not an error. Diagnostic: it says why a badge is
19
+ * missing, so a migration can be checked. */
20
+ errorText: string;
21
+ }
22
+ export const HEADER_BADGES_INITIAL: HeaderBadgeState = { notificationsBadge: null, chatBadge: null, cartBadge: null, errorText: "" };
23
+
24
+ /** The counts describe THIS run: a snapshot from the last one (the web store persists an app worker's state per
25
+ * account) would show another day's numbers until the first read lands. Declare them `notPersisted` and
26
+ * `notHydrated` on the service. */
27
+ export const HEADER_BADGE_KEYS: ("notificationsBadge" | "chatBadge" | "cartBadge" | "errorText")[] = ["notificationsBadge", "chatBadge", "cartBadge", "errorText"];
28
+
29
+ /** The action signatures a service that uses `headerBadges` declares in its `defineService` generic. */
30
+ // A `type`, not an `interface`: `defineService` constrains its actions to `Record<string, fn>`, which only a type
31
+ // alias satisfies (an interface has no implicit index signature).
32
+ export type HeaderBadgeActionSignatures<S> = {
33
+ /** Re-read the header badge counts. Only the newest read lands. */
34
+ refreshBadges: (s: S) => Promise<S>;
35
+ /** Drop every badge count (a new identity), committed at once so no stale number waits on a network read. */
36
+ dropBadges: (s: S) => Promise<S>;
37
+ /** Reserved lifecycle hooks the app-service host delivers to every service that declares them. */
38
+ onForeground: (s: S) => Promise<S>;
39
+ onIdentityChange: (s: S) => Promise<S>;
40
+ };
41
+
42
+ /** What `headerBadges` hands a worker. Named (not inferred) so a worker can annotate its `badges`, which breaks the
43
+ * loop between that value and the service handle it is built from. */
44
+ export interface HeaderBadges<S> {
45
+ actions: HeaderBadgeActionSignatures<S>;
46
+ /** Call once after `defineService`: the launch read, and the session clock's notifications. */
47
+ start(): void;
48
+ /** Re-read after something that may have changed a count (a notification receipt). */
49
+ refresh(): void;
50
+ }
51
+
52
+ type Update<S> = (s: S, change: (current: S) => Promise<S>) => Promise<S>;
53
+ type Self = () => { refreshBadges(): Promise<unknown>; dropBadges(): Promise<unknown> };
54
+
55
+ /**
56
+ * @param update serializes state changes for the worker, so overlapping actions build on the preceding result
57
+ * @param self the worker's own service handle (read lazily: it does not exist yet when this is called)
58
+ */
59
+ export function headerBadges<S extends HeaderBadgeState>(update: Update<S>, self: Self): HeaderBadges<S> {
60
+ /** Which read is the newest: an older one that finishes later must not overwrite a newer answer. */
61
+ let read = 0;
62
+ /** The host session generation the badges on show were read under. */
63
+ let generation = -1;
64
+ /** The clock the native runtime publishes for the signed-in identity: it names the site the bridge is bound to
65
+ * and notifies on every sign-in, sign-out and member switch. A host without one (web, older binaries) reads a
66
+ * signed-out stub that never notifies. */
67
+ const session = hostSessionFromGlobal();
68
+ const cleared = (current: S): S => ({ ...current, ...HEADER_BADGES_INITIAL });
69
+
70
+ const actions: HeaderBadgeActionSignatures<S> = {
71
+ refreshBadges: async (s) => {
72
+ const mine = ++read;
73
+ const badges = await readSourceBadges(session.site());
74
+ return update(s, async (current) => mine === read
75
+ ? { ...current, notificationsBadge: badges.notifications, chatBadge: badges.chat, cartBadge: badges.cart, errorText: badges.failed.length ? `could not read: ${badges.failed.join(", ")}` : "" }
76
+ : current);
77
+ },
78
+ dropBadges: async (s) => {
79
+ ++read; // a read already in flight belongs to the previous identity
80
+ return update(s, async (current) => cleared(current));
81
+ },
82
+ onForeground: async (s) => update(s, async (current) => current).finally(() => { void self().refreshBadges(); }),
83
+ onIdentityChange: async (s) => {
84
+ ++read;
85
+ const dropped = await update(s, async (current) => cleared(current));
86
+ void self().refreshBadges();
87
+ return dropped;
88
+ },
89
+ };
90
+
91
+ /** A session notification: a new generation is a new identity, so the previous one's numbers go first. */
92
+ async function onSession(): Promise<void> {
93
+ const now = session.current().generation;
94
+ if (now !== generation) {
95
+ generation = now;
96
+ await self().dropBadges();
97
+ }
98
+ await self().refreshBadges();
99
+ }
100
+
101
+ return {
102
+ actions,
103
+ /** Call once after `defineService`: the launch read, and the session clock's notifications. */
104
+ start(): void {
105
+ // Subscribing notifies at once, which is the launch read; a host with no clock needs the explicit one.
106
+ let notified = false;
107
+ session.subscribe(() => { notified = true; void onSession(); });
108
+ if (!notified) void self().refreshBadges();
109
+ },
110
+ /** Re-read after something that may have changed a count (a notification receipt). */
111
+ refresh(): void { void self().refreshBadges(); },
112
+ };
113
+ }
@@ -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
+ }
@@ -0,0 +1,70 @@
1
+ // The live counts behind the header buttons of a migrated One App (notifications, chat, cart), read for the
2
+ // app-level nav worker. `null` is "unavailable" and never a zero: a failed, offline or stale read leaves the
3
+ // badge off instead of claiming an empty inbox or cart. The shell shows no badge for null or 0 alike, so the
4
+ // two differ only in what the worker may later believe about the source.
5
+ import { getCurrentCart } from "@himalaya/wix-ecom";
6
+ import { countUnseenNotifications } from "@himalaya/wix-members-feed-viewer";
7
+ import { connectivity } from "@himalaya/state";
8
+ import { chatIdentity, unreadRoomCount } from "./business-chat-transport.js";
9
+ import { memberAccess } from "./member-access.js";
10
+
11
+ export type SourceBadgeKey = "notifications" | "chat" | "cart";
12
+ export interface SourceBadges {
13
+ notifications: number | null;
14
+ chat: number | null;
15
+ cart: number | null;
16
+ /** The counts a read was attempted for and failed: not the ones unavailable by design (a Visitor's bell and chat,
17
+ * offline), and not one dropped because the identity changed mid-read. */
18
+ failed: SourceBadgeKey[];
19
+ }
20
+ export const NO_SOURCE_BADGES: SourceBadges = { notifications: null, chat: null, cart: null, failed: [] };
21
+
22
+ /** The same item count the Shop and Account badges show: every line's quantity, a missing one counting as 1. */
23
+ async function cartItemCount(): Promise<number> {
24
+ const cart = await getCurrentCart();
25
+ return (cart.lineItems ?? []).reduce((total, line) => total + (line.quantity ?? 1), 0);
26
+ }
27
+
28
+ /** Whether the shopper is still the one the read started for. A cart belongs to a Visitor as much as to a
29
+ * Member, so this compares what `memberAccess` reports rather than requiring a signed-in member. */
30
+ async function sameShopper(started: { email: string; scope: string }): Promise<boolean> {
31
+ const now = await memberAccess();
32
+ return now.email === started.email && now.scope === started.scope;
33
+ }
34
+
35
+ async function unavailable<T>(read: () => Promise<T>): Promise<T | null> {
36
+ try { return await read(); } catch { return null; }
37
+ }
38
+
39
+ /** A read dropped because the shopper changed while it ran: expected, not a failure to report. */
40
+ const SHOPPER_CHANGED = new Error("shopper changed");
41
+
42
+ /** One refresh. Notifications and chat are a Member's: a Visitor has neither inbox, so both read null without
43
+ * a request. Notifications also need `site`, the site the native bridge is bound to (the host session's own,
44
+ * never a caller's choice); without one that counter stays unavailable. Cart is the current shopper's.
45
+ * Offline reads nothing. A count that finishes after the signed-in identity changed is dropped, so one
46
+ * member's number never lands on the next member's header. */
47
+ export async function readSourceBadges(site: string | null): Promise<SourceBadges> {
48
+ // Whatever goes wrong, the answer is "unavailable": this runs fire-and-forget at launch.
49
+ const read = unavailable(async (): Promise<SourceBadges> => {
50
+ if (!connectivity().online) return NO_SOURCE_BADGES;
51
+ const identity = await chatIdentity();
52
+ const member = identity.authenticated;
53
+ const stillHere = async <T>(count: Promise<T>): Promise<T> => {
54
+ const value = await count;
55
+ if (!(await sameShopper(identity))) throw SHOPPER_CHANGED;
56
+ return value;
57
+ };
58
+ const failed: SourceBadgeKey[] = [];
59
+ const attempt = async <T>(key: SourceBadgeKey, run: () => Promise<T>): Promise<T | null> => {
60
+ try { return await run(); } catch (error) { if (error !== SHOPPER_CHANGED) failed.push(key); return null; }
61
+ };
62
+ const [notifications, chat, cart] = await Promise.all([
63
+ member && site ? attempt("notifications", () => stillHere(countUnseenNotifications({ site }))) : null,
64
+ member ? attempt("chat", () => unreadRoomCount(identity)) : null,
65
+ attempt("cart", () => stillHere(cartItemCount())),
66
+ ]);
67
+ return { notifications, chat, cart, failed };
68
+ });
69
+ return (await read) ?? NO_SOURCE_BADGES;
70
+ }
@@ -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
  }