musora-content-services 2.168.0 → 2.169.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.
@@ -0,0 +1,33 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(rg:*)",
5
+ "Bash(npm run lint:*)",
6
+ "Bash(ls:*)",
7
+ "Bash(npm test *)",
8
+ "Bash(npx jest *)"
9
+ ],
10
+ "deny": []
11
+ },
12
+ "enabledMcpjsonServers": [
13
+ "1password",
14
+ "atlassian",
15
+ "figma",
16
+ "google-workspace",
17
+ "snowflake",
18
+ "aws",
19
+ "hex",
20
+ "sanity",
21
+ "mysql",
22
+ "slack",
23
+ "langfuse",
24
+ "chrome-devtools",
25
+ "railway",
26
+ "github",
27
+ "asana",
28
+ "nightwatch",
29
+ "mysql-staging",
30
+ "mysql-local",
31
+ "mobile"
32
+ ]
33
+ }
Binary file
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [2.169.0](https://github.com/railroadmedia/musora-content-services/compare/v2.168.0...v2.169.0) (2026-08-04)
6
+
7
+
8
+ ### Features
9
+
10
+ * **BEHSTP-415:** MCS public announcement fetch fn's ([#1010](https://github.com/railroadmedia/musora-content-services/issues/1010)) ([d504fb1](https://github.com/railroadmedia/musora-content-services/commit/d504fb105ba5303bfe7327ddef24fe468bdec374))
11
+
5
12
  ## [2.168.0](https://github.com/railroadmedia/musora-content-services/compare/v2.167.6...v2.168.0) (2026-07-30)
6
13
 
7
14
 
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.168.0",
3
+ "version": "2.169.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -45,6 +45,7 @@
45
45
  "ts-jest": "^29.2.4"
46
46
  },
47
47
  "dependencies": {
48
+ "@portabletext/to-html": "^2.0.14",
48
49
  "@sanity/client": "^5.4.2",
49
50
  "dayjs": "^1.11.13",
50
51
  "docdash": "^2.0.2",
package/src/index.d.ts CHANGED
@@ -235,6 +235,28 @@ import {
235
235
  updateMultiUserAccount
236
236
  } from './services/multi-user-accounts/multi-user-accounts.ts';
237
237
 
238
+ import {
239
+ deleteAllNotifications,
240
+ deleteNotification,
241
+ fetchLiveEventPollingState,
242
+ fetchNotificationSettings,
243
+ fetchNotifications,
244
+ fetchUnreadCount,
245
+ markAllNotificationsAsRead,
246
+ markNotificationAsRead,
247
+ markNotificationAsUnread,
248
+ pauseLiveEventPolling,
249
+ restoreNotification,
250
+ startLiveEventPolling,
251
+ updateNotificationSetting
252
+ } from './services/notifications/notifications.js';
253
+
254
+ import {
255
+ blockContentToHtml,
256
+ fetchAllPublicAnnouncements,
257
+ fetchPublicAnnouncement
258
+ } from './services/notifications/public-announcements.ts';
259
+
238
260
  import {
239
261
  getRecentActivityOffline
240
262
  } from './services/offline/activities.ts';
@@ -439,22 +461,6 @@ import {
439
461
  upgradeSubscription
440
462
  } from './services/user/memberships.ts';
441
463
 
442
- import {
443
- deleteAllNotifications,
444
- deleteNotification,
445
- fetchLiveEventPollingState,
446
- fetchNotificationSettings,
447
- fetchNotifications,
448
- fetchUnreadCount,
449
- markAllNotificationsAsRead,
450
- markNotificationAsRead,
451
- markNotificationAsUnread,
452
- pauseLiveEventPolling,
453
- restoreNotification,
454
- startLiveEventPolling,
455
- updateNotificationSetting
456
- } from './services/user/notifications.js';
457
-
458
464
  import {
459
465
  getOnboardingRecommendedContent,
460
466
  getOnboardingStatus,
@@ -534,6 +540,7 @@ declare module 'musora-content-services' {
534
540
  applyCloudflareWrapper,
535
541
  applySanityTransformations,
536
542
  assignModeratorToComment,
543
+ blockContentToHtml,
537
544
  blockUser,
538
545
  blockedUsers,
539
546
  buildEntityAndTotalQuery,
@@ -585,6 +592,7 @@ declare module 'musora-content-services' {
585
592
  extractSanityUrl,
586
593
  fetchAll,
587
594
  fetchAllFilterOptions,
595
+ fetchAllPublicAnnouncements,
588
596
  fetchArtistBySlug,
589
597
  fetchArtistLessons,
590
598
  fetchArtists,
@@ -649,6 +657,7 @@ declare module 'musora-content-services' {
649
657
  fetchPost,
650
658
  fetchPosts,
651
659
  fetchPracticeGoals,
660
+ fetchPublicAnnouncement,
652
661
  fetchRecent,
653
662
  fetchRecentActivitiesActiveTabs,
654
663
  fetchRecentUserActivities,
package/src/index.js CHANGED
@@ -239,6 +239,28 @@ import {
239
239
  updateMultiUserAccount
240
240
  } from './services/multi-user-accounts/multi-user-accounts.ts';
241
241
 
242
+ import {
243
+ deleteAllNotifications,
244
+ deleteNotification,
245
+ fetchLiveEventPollingState,
246
+ fetchNotificationSettings,
247
+ fetchNotifications,
248
+ fetchUnreadCount,
249
+ markAllNotificationsAsRead,
250
+ markNotificationAsRead,
251
+ markNotificationAsUnread,
252
+ pauseLiveEventPolling,
253
+ restoreNotification,
254
+ startLiveEventPolling,
255
+ updateNotificationSetting
256
+ } from './services/notifications/notifications.js';
257
+
258
+ import {
259
+ blockContentToHtml,
260
+ fetchAllPublicAnnouncements,
261
+ fetchPublicAnnouncement
262
+ } from './services/notifications/public-announcements.ts';
263
+
242
264
  import {
243
265
  getRecentActivityOffline
244
266
  } from './services/offline/activities.ts';
@@ -443,22 +465,6 @@ import {
443
465
  upgradeSubscription
444
466
  } from './services/user/memberships.ts';
445
467
 
446
- import {
447
- deleteAllNotifications,
448
- deleteNotification,
449
- fetchLiveEventPollingState,
450
- fetchNotificationSettings,
451
- fetchNotifications,
452
- fetchUnreadCount,
453
- markAllNotificationsAsRead,
454
- markNotificationAsRead,
455
- markNotificationAsUnread,
456
- pauseLiveEventPolling,
457
- restoreNotification,
458
- startLiveEventPolling,
459
- updateNotificationSetting
460
- } from './services/user/notifications.js';
461
-
462
468
  import {
463
469
  getOnboardingRecommendedContent,
464
470
  getOnboardingStatus,
@@ -533,6 +539,7 @@ export {
533
539
  applyCloudflareWrapper,
534
540
  applySanityTransformations,
535
541
  assignModeratorToComment,
542
+ blockContentToHtml,
536
543
  blockUser,
537
544
  blockedUsers,
538
545
  buildEntityAndTotalQuery,
@@ -584,6 +591,7 @@ export {
584
591
  extractSanityUrl,
585
592
  fetchAll,
586
593
  fetchAllFilterOptions,
594
+ fetchAllPublicAnnouncements,
587
595
  fetchArtistBySlug,
588
596
  fetchArtistLessons,
589
597
  fetchArtists,
@@ -648,6 +656,7 @@ export {
648
656
  fetchPost,
649
657
  fetchPosts,
650
658
  fetchPracticeGoals,
659
+ fetchPublicAnnouncement,
651
660
  fetchRecent,
652
661
  fetchRecentActivitiesActiveTabs,
653
662
  fetchRecentUserActivities,
@@ -1,4 +1,4 @@
1
- import {fetchLiveEventPollingState, fetchUnreadCount, pauseLiveEventPolling} from "./user/notifications"
1
+ import {fetchLiveEventPollingState, fetchUnreadCount, pauseLiveEventPolling} from "./notifications/notifications.js"
2
2
  import {fetchLiveEvent} from "./sanity"
3
3
  import { DataContext, PollingStateVersionKey } from './dataContext'
4
4
 
@@ -3,9 +3,9 @@
3
3
  */
4
4
  import { GET, PUT, DELETE } from '../../infrastructure/http/HttpClient.ts'
5
5
  import eventsAPI from '../eventsAPI.js'
6
- import './types.js'
7
- import {globalConfig} from "../config";
8
- import {fetchLiveEvent} from "../sanity";
6
+ import '../user/types.js'
7
+ import {globalConfig} from "../config.js";
8
+ import {fetchLiveEvent} from "../sanity.js";
9
9
 
10
10
  const baseUrl = `/api/notifications`
11
11
 
@@ -0,0 +1,234 @@
1
+ import { fetchSanity, getSanityDate } from '../sanity.js'
2
+ import { globalConfig } from '../config.js'
3
+
4
+ interface PublicAnnouncement {
5
+ title: string
6
+ slug: string
7
+ published_on: string
8
+ description: string
9
+ message: string
10
+ forum_data?: Record<string, ForumData>
11
+ }
12
+
13
+ interface ForumData {
14
+ category_id: string
15
+ forum_id: string
16
+ url: string
17
+ }
18
+
19
+ /**
20
+ * Fetches a specific public announcement by slug, or by default the most recent published one.
21
+ * @param {string|null} slug
22
+ * @returns {Promise<PublicAnnouncement|null>} The public announcement object, or null if not found
23
+ */
24
+ export async function fetchPublicAnnouncement(slug?: string): Promise<PublicAnnouncement | null> {
25
+ const now = getSanityDate(new Date())
26
+
27
+ const slugString = slug ? `&& slug.current == '${slug}'` : ''
28
+
29
+ // grab the specific one by slug, or the most recent one if no slug is provided
30
+ const query = `*[_type == 'public-announcement' && published_on <= '${now}' ${slugString}]
31
+ {
32
+ title,
33
+ 'slug': slug.current,
34
+ published_on,
35
+ description,
36
+ message,
37
+ forum_data
38
+ }
39
+ | order(published_on desc)`
40
+
41
+ const result = await fetchSanity(query, false, {processNeedAccess: false, processPageType: false})
42
+
43
+ if (result?.message) {
44
+ result.message = blockContentToHtml(result.message)
45
+ }
46
+
47
+ return result
48
+ }
49
+
50
+ /**
51
+ * Fetches all public announcements published within the last `spanDays` days (default 1 year).
52
+ * @param {number|null} spanDays - Number of days to look back for public announcements (default: 1 year)
53
+ * @returns {Promise<PublicAnnouncement[]|null>} Array of public announcement objects, or null if none found
54
+ */
55
+ export async function fetchAllPublicAnnouncements(spanDays: number = 365): Promise<PublicAnnouncement[] | null> {
56
+ const rawNowDate = new Date()
57
+ const rawNow = new Date().getTime()
58
+ const nowDate = getSanityDate(rawNowDate)
59
+ const startDate = getSanityDate(new Date(rawNow - spanDays * 24 * 60 * 60 * 1000))
60
+
61
+ const query = `*[_type == 'public-announcement' && published_on >= '${startDate}' && published_on <= '${nowDate}']
62
+ {
63
+ title,
64
+ 'slug': slug.current,
65
+ published_on
66
+ }
67
+ | order(published_on desc)`
68
+
69
+ return await fetchSanity(query, true, {processNeedAccess: false, processPageType: false})
70
+ }
71
+
72
+ interface ListStackEntry {
73
+ tag: 'ul' | 'ol'
74
+ level: number
75
+ }
76
+
77
+ /**
78
+ * @param {Array<Object>} blocks - Sanity Portable Text / block content array
79
+ * @returns {string} HTML string
80
+ */
81
+ export function blockContentToHtml(blocks: any[]): string {
82
+ if (!blocks) {
83
+ return ''
84
+ }
85
+
86
+ let html = ''
87
+ const listStack: ListStackEntry[] = []
88
+
89
+ for (const block of blocks) {
90
+ const listItem = block.listItem ?? null
91
+
92
+ if (!listItem) {
93
+ html += closeLists(listStack, 0)
94
+ html += block._type === 'image' ? renderImage(block) : renderBlock(block)
95
+ continue
96
+ }
97
+
98
+ const level = block.level ?? 1
99
+ const tag: 'ul' | 'ol' = listItem === 'number' ? 'ol' : 'ul'
100
+
101
+ while (
102
+ listStack.length &&
103
+ (listStack[listStack.length - 1].level > level ||
104
+ (listStack[listStack.length - 1].level === level && listStack[listStack.length - 1].tag !== tag))
105
+ ) {
106
+ const closed = listStack.pop()
107
+ html += `</${closed.tag}>`
108
+ }
109
+
110
+ while (!listStack.length || listStack[listStack.length - 1].level < level) {
111
+ listStack.push({ tag, level: listStack.length + 1 })
112
+ html += `<${tag}>`
113
+ }
114
+
115
+ html += `<li>${renderInline(block)}</li>`
116
+ }
117
+
118
+ html += closeLists(listStack, 0)
119
+ return html
120
+ }
121
+
122
+ function closeLists(listStack: ListStackEntry[], downToLevel: number): string {
123
+ let html = ''
124
+ while (listStack.length && listStack[listStack.length - 1].level > downToLevel) {
125
+ const closed = listStack.pop()
126
+ html += `</${closed.tag}>`
127
+ }
128
+ return html
129
+ }
130
+
131
+ function renderInline(block: any): string {
132
+ const markDefsByKey: Record<string, any> = {}
133
+ for (const markDef of block.markDefs ?? []) {
134
+ markDefsByKey[markDef._key] = markDef
135
+ }
136
+
137
+ let inner = ''
138
+ for (const child of block.children ?? []) {
139
+ inner += renderSpan(child, markDefsByKey)
140
+ }
141
+
142
+ return inner === '' ? '&nbsp;' : inner
143
+ }
144
+
145
+ function renderBlock(block: any): string {
146
+ const tag = ((): string => {
147
+ switch (block.style ?? 'normal') {
148
+ case 'h1':
149
+ return 'h1'
150
+ case 'h2':
151
+ return 'h2'
152
+ case 'h3':
153
+ return 'h3'
154
+ case 'h4':
155
+ return 'h4'
156
+ case 'blockquote':
157
+ return 'blockquote'
158
+ default:
159
+ return 'p'
160
+ }
161
+ })()
162
+
163
+ const inner = renderInline(block)
164
+
165
+ return `<${tag}>${inner}</${tag}>`
166
+ }
167
+
168
+ function renderSpan(child: any, markDefsByKey: Record<string, any>): string {
169
+ let text = escapeHtml(child.text ?? '')
170
+ text = text.replace(/\n/g, '<br />')
171
+
172
+ for (const mark of child.marks ?? []) {
173
+ switch (true) {
174
+ case mark === 'strong':
175
+ text = `<strong>${text}</strong>`
176
+ break
177
+ case mark === 'em':
178
+ text = `<em>${text}</em>`
179
+ break
180
+ case mark === 'underline':
181
+ text = `<u>${text}</u>`
182
+ break
183
+ case mark === 'code':
184
+ text = `<code>${text}</code>`
185
+ break
186
+ case markDefsByKey[mark] !== undefined:
187
+ text = renderMarkDef(markDefsByKey[mark], text)
188
+ break
189
+ }
190
+ }
191
+
192
+ return text
193
+ }
194
+
195
+ function renderMarkDef(markDef: any, text: string): string {
196
+ if (markDef?._type !== 'link') {
197
+ return text
198
+ }
199
+ const href = escapeHtml(markDef.href ?? '')
200
+ return `<a href="${href}">${text}</a>`
201
+ }
202
+
203
+ function renderImage(block: any): string {
204
+ const ref = block.asset?._ref ?? null
205
+ const percent = block.display_width_percent ?? 100
206
+
207
+ const url = sanityImageRefToUrl(ref)
208
+ if (!url) {
209
+ return ''
210
+ }
211
+
212
+ return percent < 100
213
+ ? `<img src="${url}" style="width:${percent}%;max-width:100%;" />`
214
+ : `<img src="${url}" />`
215
+ }
216
+
217
+ function escapeHtml(text: string): string {
218
+ return text
219
+ .replace(/&/g, '&amp;')
220
+ .replace(/"/g, '&quot;')
221
+ .replace(/'/g, '&#039;')
222
+ .replace(/</g, '&lt;')
223
+ .replace(/>/g, '&gt;')
224
+ }
225
+
226
+ function sanityImageRefToUrl(ref: string | null | undefined): string | null {
227
+ const match = ref?.match(/^image-([a-f0-9]+)-(\d+x\d+)-(\w+)$/)
228
+ if (!match) {
229
+ return null
230
+ }
231
+ const [, assetId, dimensions, format] = match
232
+ const { projectId, dataset } = globalConfig.sanityConfig
233
+ return `https://cdn.sanity.io/images/${projectId}/${dataset}/${assetId}-${dimensions}.${format}`
234
+ }
@@ -1 +1 @@
1
- export const MCS_VERSION = '2.168.0'
1
+ export const MCS_VERSION = '2.169.0'
@@ -1,61 +0,0 @@
1
- <component name="ProjectCodeStyleConfiguration">
2
- <code_scheme name="Project" version="173">
3
- <HTMLCodeStyleSettings>
4
- <option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
5
- </HTMLCodeStyleSettings>
6
- <JSCodeStyleSettings version="0">
7
- <option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
8
- <option name="FORCE_SEMICOLON_STYLE" value="true" />
9
- <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
10
- <option name="USE_DOUBLE_QUOTES" value="false" />
11
- <option name="FORCE_QUOTE_STYlE" value="true" />
12
- <option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
13
- <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
14
- <option name="SPACES_WITHIN_IMPORTS" value="true" />
15
- </JSCodeStyleSettings>
16
- <TypeScriptCodeStyleSettings version="0">
17
- <option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
18
- <option name="FORCE_SEMICOLON_STYLE" value="true" />
19
- <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
20
- <option name="USE_DOUBLE_QUOTES" value="false" />
21
- <option name="FORCE_QUOTE_STYlE" value="true" />
22
- <option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
23
- <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
24
- <option name="SPACES_WITHIN_IMPORTS" value="true" />
25
- </TypeScriptCodeStyleSettings>
26
- <VueCodeStyleSettings>
27
- <option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
28
- <option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
29
- </VueCodeStyleSettings>
30
- <codeStyleSettings language="HTML">
31
- <option name="SOFT_MARGINS" value="100" />
32
- <indentOptions>
33
- <option name="INDENT_SIZE" value="2" />
34
- <option name="CONTINUATION_INDENT_SIZE" value="2" />
35
- <option name="TAB_SIZE" value="2" />
36
- </indentOptions>
37
- </codeStyleSettings>
38
- <codeStyleSettings language="JavaScript">
39
- <option name="SOFT_MARGINS" value="100" />
40
- <indentOptions>
41
- <option name="INDENT_SIZE" value="2" />
42
- <option name="CONTINUATION_INDENT_SIZE" value="2" />
43
- <option name="TAB_SIZE" value="2" />
44
- </indentOptions>
45
- </codeStyleSettings>
46
- <codeStyleSettings language="TypeScript">
47
- <option name="SOFT_MARGINS" value="100" />
48
- <indentOptions>
49
- <option name="INDENT_SIZE" value="2" />
50
- <option name="CONTINUATION_INDENT_SIZE" value="2" />
51
- <option name="TAB_SIZE" value="2" />
52
- </indentOptions>
53
- </codeStyleSettings>
54
- <codeStyleSettings language="Vue">
55
- <option name="SOFT_MARGINS" value="100" />
56
- <indentOptions>
57
- <option name="CONTINUATION_INDENT_SIZE" value="2" />
58
- </indentOptions>
59
- </codeStyleSettings>
60
- </code_scheme>
61
- </component>
@@ -1,5 +0,0 @@
1
- <component name="ProjectCodeStyleConfiguration">
2
- <state>
3
- <option name="USE_PER_PROJECT_SETTINGS" value="true" />
4
- </state>
5
- </component>
package/.idea/modules.xml DELETED
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ProjectModuleManager">
4
- <modules>
5
- <module fileurl="file://$PROJECT_DIR$/.idea/musora-content-services.iml" filepath="$PROJECT_DIR$/.idea/musora-content-services.iml" />
6
- </modules>
7
- </component>
8
- </project>
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <module type="WEB_MODULE" version="4">
3
- <component name="NewModuleRootManager">
4
- <content url="file://$MODULE_DIR$" />
5
- <orderEntry type="inheritedJdk" />
6
- <orderEntry type="sourceFolder" forTests="false" />
7
- </component>
8
- </module>
package/.idea/php.xml DELETED
@@ -1,19 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="MessDetectorOptionsConfiguration">
4
- <option name="transferred" value="true" />
5
- </component>
6
- <component name="PHPCSFixerOptionsConfiguration">
7
- <option name="transferred" value="true" />
8
- </component>
9
- <component name="PHPCodeSnifferOptionsConfiguration">
10
- <option name="highlightLevel" value="WARNING" />
11
- <option name="transferred" value="true" />
12
- </component>
13
- <component name="PhpStanOptionsConfiguration">
14
- <option name="transferred" value="true" />
15
- </component>
16
- <component name="PsalmOptionsConfiguration">
17
- <option name="transferred" value="true" />
18
- </component>
19
- </project>
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="PrettierConfiguration">
4
- <option name="myConfigurationMode" value="AUTOMATIC" />
5
- </component>
6
- </project>
package/.idea/vcs.xml DELETED
@@ -1,7 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="VcsDirectoryMappings">
4
- <mapping directory="" vcs="Git" />
5
- <mapping directory="$PROJECT_DIR$" vcs="Git" />
6
- </component>
7
- </project>