@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
@@ -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
  }
@@ -27970,6 +27970,72 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
27970
27970
  }
27971
27971
  }
27972
27972
  }
27973
+ },
27974
+ {
27975
+ "id": "",
27976
+ "body": {
27977
+ "package": "himalaya",
27978
+ "type": "HStack",
27979
+ "props": {
27980
+ "spacing": 8,
27981
+ "children": [
27982
+ {
27983
+ "id": "himi_blog_post.report",
27984
+ "body": {
27985
+ "package": "himalaya",
27986
+ "type": "Button",
27987
+ "props": {
27988
+ "title": "Report",
27989
+ "style": "secondary",
27990
+ "fullWidth": true,
27991
+ "action": {
27992
+ "actionId": "tier5.himiBlogPost.report"
27993
+ }
27994
+ }
27995
+ },
27996
+ "modifiers": {
27997
+ "hiddenPath": "$service:himiBlogPost.reported"
27998
+ }
27999
+ },
28000
+ {
28001
+ "id": "himi_blog_post.hide",
28002
+ "body": {
28003
+ "package": "himalaya",
28004
+ "type": "Button",
28005
+ "props": {
28006
+ "title": "Hide post",
28007
+ "style": "secondary",
28008
+ "fullWidth": true,
28009
+ "action": {
28010
+ "actionId": "tier5.himiBlogPost.openHide"
28011
+ }
28012
+ }
28013
+ },
28014
+ "modifiers": {
28015
+ "hiddenPath": "$service:himiBlogPost.hidden"
28016
+ }
28017
+ },
28018
+ {
28019
+ "id": "himi_blog_post.unhide",
28020
+ "body": {
28021
+ "package": "himalaya",
28022
+ "type": "Button",
28023
+ "props": {
28024
+ "title": "Undo hide",
28025
+ "style": "secondary",
28026
+ "fullWidth": true,
28027
+ "action": {
28028
+ "actionId": "tier5.himiBlogPost.unhide"
28029
+ }
28030
+ }
28031
+ },
28032
+ "modifiers": {
28033
+ "hiddenPath": "$service:himiBlogPost.notHidden"
28034
+ }
28035
+ }
28036
+ ]
28037
+ }
28038
+ }
27973
28039
  }
27974
28040
  ]
27975
28041
  }
@@ -28228,6 +28294,122 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
28228
28294
  "hiddenPath": "$service:himiBlogPost.noPost"
28229
28295
  }
28230
28296
  },
28297
+ {
28298
+ "id": "himi_blog_post.hide_sheet",
28299
+ "body": {
28300
+ "package": "himalaya",
28301
+ "type": "BottomSheet",
28302
+ "props": {
28303
+ "presentedPath": "$service:himiBlogPost.hideSheetOpen",
28304
+ "dismissible": true,
28305
+ "showGrabber": true,
28306
+ "onDismiss": {
28307
+ "actionId": "tier5.himiBlogPost.closeHide"
28308
+ },
28309
+ "content": {
28310
+ "id": "",
28311
+ "body": {
28312
+ "package": "himalaya",
28313
+ "type": "VStack",
28314
+ "props": {
28315
+ "spacing": 12,
28316
+ "alignment": "leading",
28317
+ "children": [
28318
+ {
28319
+ "id": "",
28320
+ "body": {
28321
+ "package": "himalaya",
28322
+ "type": "Text",
28323
+ "props": {
28324
+ "text": "Why are you hiding this post?",
28325
+ "style": "title3"
28326
+ }
28327
+ }
28328
+ },
28329
+ {
28330
+ "id": "himi_blog_post.hide_offensive",
28331
+ "body": {
28332
+ "package": "himalaya",
28333
+ "type": "Button",
28334
+ "props": {
28335
+ "title": "It is offensive",
28336
+ "style": "secondary",
28337
+ "fullWidth": true,
28338
+ "action": {
28339
+ "actionId": "tier5.himiBlogPost.hide",
28340
+ "params": {
28341
+ "reason": "offensive"
28342
+ }
28343
+ }
28344
+ }
28345
+ }
28346
+ },
28347
+ {
28348
+ "id": "himi_blog_post.hide_misleading",
28349
+ "body": {
28350
+ "package": "himalaya",
28351
+ "type": "Button",
28352
+ "props": {
28353
+ "title": "It is misleading",
28354
+ "style": "secondary",
28355
+ "fullWidth": true,
28356
+ "action": {
28357
+ "actionId": "tier5.himiBlogPost.hide",
28358
+ "params": {
28359
+ "reason": "misleading"
28360
+ }
28361
+ }
28362
+ }
28363
+ }
28364
+ },
28365
+ {
28366
+ "id": "himi_blog_post.hide_other",
28367
+ "body": {
28368
+ "package": "himalaya",
28369
+ "type": "Button",
28370
+ "props": {
28371
+ "title": "Something else",
28372
+ "style": "secondary",
28373
+ "fullWidth": true,
28374
+ "action": {
28375
+ "actionId": "tier5.himiBlogPost.hide",
28376
+ "params": {
28377
+ "reason": "other"
28378
+ }
28379
+ }
28380
+ }
28381
+ }
28382
+ },
28383
+ {
28384
+ "id": "himi_blog_post.hide_cancel",
28385
+ "body": {
28386
+ "package": "himalaya",
28387
+ "type": "Button",
28388
+ "props": {
28389
+ "title": "Cancel",
28390
+ "style": "secondary",
28391
+ "fullWidth": true,
28392
+ "action": {
28393
+ "actionId": "tier5.himiBlogPost.closeHide"
28394
+ }
28395
+ }
28396
+ }
28397
+ }
28398
+ ]
28399
+ }
28400
+ },
28401
+ "modifiers": {
28402
+ "padding": {
28403
+ "leading": 20,
28404
+ "trailing": 20,
28405
+ "top": 16,
28406
+ "bottom": 24
28407
+ }
28408
+ }
28409
+ }
28410
+ }
28411
+ }
28412
+ },
28231
28413
  {
28232
28414
  "id": "",
28233
28415
  "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
  }