@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.
- package/dist/cli.mjs +1351 -977
- package/guides/himi-json-output.md +25 -1
- package/guides/one-app-to-himi-app.md +33 -11
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +182 -0
- package/templates/beauty-salon/tier5-src/_shared/himi-blog-hidden.ts +66 -0
- package/templates/beauty-salon/tier5-src/himi-blog-feed/index.ts +3 -1
- package/templates/beauty-salon/tier5-src/himi-blog-post/index.ts +42 -0
- package/templates/beauty-salon/tier5-src/himi-blog-saved/index.ts +4 -1
- package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +5 -2
- package/templates/branded-lite/dev/index.ts +182 -0
- package/templates/branded-lite/l10n/en.json +10 -7
- package/templates/branded-lite/tier5-src/_shared/header-badges.ts +113 -0
- package/templates/branded-lite/tier5-src/_shared/himi-blog-hidden.ts +66 -0
- package/templates/branded-lite/tier5-src/_shared/source-badges.ts +70 -0
- package/templates/branded-lite/tier5-src/himi-blog-feed/index.ts +3 -1
- package/templates/branded-lite/tier5-src/himi-blog-post/index.ts +42 -0
- package/templates/branded-lite/tier5-src/himi-blog-saved/index.ts +4 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-header-badges/index.ts +38 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +5 -2
- package/templates/community-club/dev/index.ts +182 -0
- package/templates/community-club/tier5-src/_shared/himi-blog-hidden.ts +66 -0
- package/templates/community-club/tier5-src/himi-blog-feed/index.ts +3 -1
- package/templates/community-club/tier5-src/himi-blog-post/index.ts +42 -0
- package/templates/community-club/tier5-src/himi-blog-saved/index.ts +4 -1
- package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +5 -2
- package/templates/events-meetups/dev/index.ts +182 -0
- package/templates/events-meetups/tier5-src/_shared/himi-blog-hidden.ts +66 -0
- package/templates/events-meetups/tier5-src/himi-blog-feed/index.ts +3 -1
- package/templates/events-meetups/tier5-src/himi-blog-post/index.ts +42 -0
- package/templates/events-meetups/tier5-src/himi-blog-saved/index.ts +4 -1
- package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +5 -2
- package/templates/gym-fitness/dev/index.ts +182 -0
- package/templates/gym-fitness/tier5-src/_shared/himi-blog-hidden.ts +66 -0
- package/templates/gym-fitness/tier5-src/himi-blog-feed/index.ts +3 -1
- package/templates/gym-fitness/tier5-src/himi-blog-post/index.ts +42 -0
- package/templates/gym-fitness/tier5-src/himi-blog-saved/index.ts +4 -1
- package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +5 -2
- package/templates/nonprofit-cause/dev/index.ts +182 -0
- package/templates/nonprofit-cause/tier5-src/_shared/himi-blog-hidden.ts +66 -0
- package/templates/nonprofit-cause/tier5-src/himi-blog-feed/index.ts +3 -1
- package/templates/nonprofit-cause/tier5-src/himi-blog-post/index.ts +42 -0
- package/templates/nonprofit-cause/tier5-src/himi-blog-saved/index.ts +4 -1
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +5 -2
- package/templates/podcast-creator/dev/index.ts +182 -0
- package/templates/podcast-creator/tier5-src/_shared/himi-blog-hidden.ts +66 -0
- package/templates/podcast-creator/tier5-src/himi-blog-feed/index.ts +3 -1
- package/templates/podcast-creator/tier5-src/himi-blog-post/index.ts +42 -0
- package/templates/podcast-creator/tier5-src/himi-blog-saved/index.ts +4 -1
- package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +5 -2
- package/templates/portfolio-creative/dev/index.ts +182 -0
- package/templates/portfolio-creative/tier5-src/_shared/himi-blog-hidden.ts +66 -0
- package/templates/portfolio-creative/tier5-src/himi-blog-feed/index.ts +3 -1
- package/templates/portfolio-creative/tier5-src/himi-blog-post/index.ts +42 -0
- package/templates/portfolio-creative/tier5-src/himi-blog-saved/index.ts +4 -1
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +5 -2
- package/templates/restaurant/dev/index.ts +182 -0
- package/templates/restaurant/tier5-src/_shared/himi-blog-hidden.ts +66 -0
- package/templates/restaurant/tier5-src/himi-blog-feed/index.ts +3 -1
- package/templates/restaurant/tier5-src/himi-blog-post/index.ts +42 -0
- package/templates/restaurant/tier5-src/himi-blog-saved/index.ts +4 -1
- package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +5 -2
- package/templates/retail-store/dev/index.ts +182 -0
- package/templates/retail-store/tier5-src/_shared/himi-blog-hidden.ts +66 -0
- package/templates/retail-store/tier5-src/himi-blog-feed/index.ts +3 -1
- package/templates/retail-store/tier5-src/himi-blog-post/index.ts +42 -0
- package/templates/retail-store/tier5-src/himi-blog-saved/index.ts +4 -1
- package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +5 -2
- package/templates/yoga-studio/dev/index.ts +182 -0
- package/templates/yoga-studio/tier5-src/_shared/himi-blog-hidden.ts +66 -0
- package/templates/yoga-studio/tier5-src/himi-blog-feed/index.ts +3 -1
- package/templates/yoga-studio/tier5-src/himi-blog-post/index.ts +42 -0
- package/templates/yoga-studio/tier5-src/himi-blog-saved/index.ts +4 -1
- 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-
|
|
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[
|
|
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[
|
|
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[
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|