+ {watchResponse.error && (
+
+ Forum notification preferences could not be loaded.
+ {' '}
+
+
+ )}
{mutationError && (
{mutationError}
diff --git a/src/apps/opportunities/src/models/forum.models.ts b/src/apps/opportunities/src/models/forum.models.ts
index 99af8743a..960f9fbc5 100644
--- a/src/apps/opportunities/src/models/forum.models.ts
+++ b/src/apps/opportunities/src/models/forum.models.ts
@@ -138,3 +138,10 @@ export interface ForumWatchMutationResponse {
topicId: string
watching?: boolean
}
+
+/** Effective current-member watch state for an entire challenge forum. */
+export interface ChallengeForumWatchState {
+ challengeId: string
+ memberId: string
+ watching: boolean
+}
diff --git a/src/apps/opportunities/src/services/forum.service.spec.ts b/src/apps/opportunities/src/services/forum.service.spec.ts
index 6aceb0fac..5f6a94700 100644
--- a/src/apps/opportunities/src/services/forum.service.spec.ts
+++ b/src/apps/opportunities/src/services/forum.service.spec.ts
@@ -5,8 +5,10 @@ import {
deleteForumPost,
deleteForumTopic,
getChallengeForumTopics,
+ getChallengeForumWatch,
getForumTopicDetail,
markForumTopicRead,
+ setChallengeForumWatching,
setForumPostReaction,
setForumTopicWatching,
updateForumPost,
@@ -34,6 +36,19 @@ jest.mock('~/libs/core', () => ({
describe('forum service', () => {
beforeEach(() => jest.clearAllMocks())
+ it('reads and persists challenge-wide watch choices without impersonation fields', async () => {
+ await getChallengeForumWatch('challenge/id')
+ await setChallengeForumWatching('challenge/id', true)
+ await setChallengeForumWatching('challenge/id', false)
+ const url = 'https://api.example/v6/forums/challenges/challenge%2Fid/watch'
+ expect(mockXhrGetAsync)
+ .toHaveBeenCalledWith(url)
+ expect(mockXhrPutAsync)
+ .toHaveBeenCalledWith(url, {})
+ expect(mockXhrDeleteAsync)
+ .toHaveBeenCalledWith(url)
+ })
+
it('builds an encoded, bounded challenge topic page URL', () => {
expect(challengeForumTopicsUrl('challenge/id', 0, 0))
.toBe('https://api.example/v6/forums/topics/challenges/challenge%2Fid?page=1&perPage=1')
diff --git a/src/apps/opportunities/src/services/forum.service.ts b/src/apps/opportunities/src/services/forum.service.ts
index b4a015f77..443d91fdb 100644
--- a/src/apps/opportunities/src/services/forum.service.ts
+++ b/src/apps/opportunities/src/services/forum.service.ts
@@ -8,6 +8,7 @@ import {
} from '~/libs/core'
import {
+ ChallengeForumWatchState,
CreateForumPostRequest,
CreateForumTopicRequest,
CreateForumTopicResponse,
@@ -275,3 +276,32 @@ export async function markForumTopicRead(topicId: string): Promise {
{},
)
}
+
+/**
+ * Loads the current member's effective challenge-wide watch preference.
+ * @param challengeId challenge whose forum preference is requested.
+ * @returns persisted preference or default derived from registration/copilot assignment.
+ * @throws Propagates authentication, authorization, and network errors.
+ */
+export function getChallengeForumWatch(challengeId: string): Promise {
+ return xhrGetAsync(
+ `${FORUMS_URL}/challenges/${encodeURIComponent(challengeId)}/watch`,
+ )
+}
+
+/**
+ * Persists the current member's watch preference for existing and future forum topics.
+ * @param challengeId challenge whose forum preference is updated.
+ * @param watching true to watch, false to persist an opt-out from default notifications.
+ * @returns saved challenge-wide watch state.
+ * @throws Propagates authentication, authorization, and network errors.
+ */
+export function setChallengeForumWatching(
+ challengeId: string,
+ watching: boolean,
+): Promise {
+ const url = `${FORUMS_URL}/challenges/${encodeURIComponent(challengeId)}/watch`
+ return watching
+ ? xhrPutAsync, ChallengeForumWatchState>(url, {})
+ : xhrDeleteAsync(url)
+}