@codingfactory/socialkit-vue 0.7.22 → 0.7.24

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 (37) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/services/circles.d.ts +7 -0
  5. package/dist/services/circles.d.ts.map +1 -1
  6. package/dist/services/circles.js +34 -5
  7. package/dist/services/circles.js.map +1 -1
  8. package/dist/stores/__tests__/discussion.spec.d.ts +2 -0
  9. package/dist/stores/__tests__/discussion.spec.d.ts.map +1 -0
  10. package/dist/stores/__tests__/discussion.spec.js +768 -0
  11. package/dist/stores/__tests__/discussion.spec.js.map +1 -0
  12. package/dist/stores/circles.d.ts +144 -0
  13. package/dist/stores/circles.d.ts.map +1 -1
  14. package/dist/stores/circles.js +7 -1
  15. package/dist/stores/circles.js.map +1 -1
  16. package/dist/stores/content.d.ts.map +1 -1
  17. package/dist/stores/content.js +6 -3
  18. package/dist/stores/content.js.map +1 -1
  19. package/dist/stores/discussion.d.ts +714 -15
  20. package/dist/stores/discussion.d.ts.map +1 -1
  21. package/dist/stores/discussion.js +274 -66
  22. package/dist/stores/discussion.js.map +1 -1
  23. package/dist/types/content.d.ts +3 -2
  24. package/dist/types/content.d.ts.map +1 -1
  25. package/dist/types/content.js +2 -1
  26. package/dist/types/content.js.map +1 -1
  27. package/dist/types/discussion.d.ts +38 -0
  28. package/dist/types/discussion.d.ts.map +1 -1
  29. package/package.json +1 -1
  30. package/src/index.ts +4 -0
  31. package/src/services/circles.ts +45 -5
  32. package/src/stores/__tests__/discussion.spec.ts +945 -0
  33. package/src/stores/circles.ts +7 -1
  34. package/src/stores/content.ts +6 -3
  35. package/src/stores/discussion.ts +335 -77
  36. package/src/types/content.ts +3 -2
  37. package/src/types/discussion.ts +43 -0
@@ -6,16 +6,21 @@ import { isAxiosError } from 'axios'
6
6
  import { defineStore } from 'pinia'
7
7
  import { onScopeDispose, ref } from 'vue'
8
8
  import type {
9
+ CreateSpaceInput,
9
10
  CreateReplyInput,
10
11
  CreateThreadInput,
12
+ DiscussionBrowseMode,
11
13
  DiscussionCategorySummary,
12
14
  DiscussionStoreConfig,
13
15
  DiscussionTag,
16
+ LoadSpacesOptions,
17
+ QuotedReply,
14
18
  QuoteResponse,
15
19
  Reply,
16
20
  SaveDraftOptions,
17
21
  Space,
18
22
  SpaceMembership,
23
+ SpaceThreadFilter,
19
24
  SpaceThreadSort,
20
25
  Thread,
21
26
  UpdateThreadInput,
@@ -252,6 +257,8 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
252
257
  const spaceMemberships = ref<Record<string, SpaceMembership>>({})
253
258
  const spaceMembershipLoading = ref<Record<string, boolean>>({})
254
259
  const threads = ref<Thread[]>([])
260
+ const browseThreadList = ref<Thread[]>([])
261
+ const currentBrowseMode = ref<DiscussionBrowseMode | null>(null)
255
262
  const currentThread = ref<Thread | null>(null)
256
263
  const replies = ref<Reply[]>([])
257
264
  const currentReplySort = ref<'best' | 'top' | 'new' | 'controversial'>('best')
@@ -261,8 +268,10 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
261
268
  const repliesLoadingState = createLoadingState()
262
269
 
263
270
  const loading = ref(false)
271
+ let activeLoadingRequests = 0
264
272
  const error = ref<string | null>(null)
265
273
  const nextCursor = ref<string | null>(null)
274
+ const browseNextCursor = ref<string | null>(null)
266
275
  const repliesNextCursor = ref<string | null>(null)
267
276
 
268
277
  const latestSpaceSlug = ref<string | null>(null)
@@ -365,7 +374,7 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
365
374
  }
366
375
  }
367
376
 
368
- function resolveQuotedReply(reply: Reply, existingReply?: Reply | null): Reply['quoted_reply'] {
377
+ function resolveQuotedReply(reply: Reply, existingReply?: Reply | null): QuotedReply | null {
369
378
  const quotedReplyId = typeof reply.quoted_reply_id === 'string'
370
379
  ? reply.quoted_reply_id.trim()
371
380
  : ''
@@ -393,6 +402,19 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
393
402
  return hasQuotedReplyBody(incomingQuotedReply) ? incomingQuotedReply : null
394
403
  }
395
404
 
405
+ /**
406
+ * Check whether a reply-like object carries a truthy `is_author` flag.
407
+ * The field is not part of the Reply TS interface (it is user-specific
408
+ * metadata injected by the API) so we access it reflectively.
409
+ */
410
+ function hasReplyAuthorFlag(reply: Reply | Record<string, unknown>): boolean {
411
+ return Reflect.get(reply as object, 'is_author') === true
412
+ }
413
+
414
+ function setReplyAuthorFlag(reply: Reply | Record<string, unknown>, value: boolean): void {
415
+ Reflect.set(reply as object, 'is_author', value)
416
+ }
417
+
396
418
  function normalizeReplyPayload(reply: Reply, existingReply?: Reply | null): Reply {
397
419
  const normalized: Reply = { ...reply }
398
420
  const hasQuotedReplyId = typeof normalized.quoted_reply_id === 'string'
@@ -651,6 +673,16 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
651
673
  REPLY: 'reply',
652
674
  } as const
653
675
 
676
+ function beginLoading(): void {
677
+ activeLoadingRequests += 1
678
+ loading.value = true
679
+ }
680
+
681
+ function endLoading(): void {
682
+ activeLoadingRequests = Math.max(0, activeLoadingRequests - 1)
683
+ loading.value = activeLoadingRequests > 0
684
+ }
685
+
654
686
  function flattenTree(tree: Space[]): Space[] {
655
687
  const result: Space[] = []
656
688
 
@@ -675,13 +707,38 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
675
707
  return spaces.value.filter((space) => space.is_leaf !== false)
676
708
  }
677
709
 
678
- async function loadSpaces(): Promise<unknown> {
710
+ function buildSpaceQueryString(options?: LoadSpacesOptions): string {
711
+ const params = new URLSearchParams()
712
+
713
+ params.set('tree', options?.tree === false ? '0' : '1')
714
+
715
+ if (options?.kind) {
716
+ params.set('kind', options.kind)
717
+ }
718
+
719
+ if (typeof options?.scope_type === 'string' && options.scope_type.trim().length > 0) {
720
+ params.set('scope_type', options.scope_type.trim())
721
+ }
722
+
723
+ if (typeof options?.scope_id === 'string' && options.scope_id.trim().length > 0) {
724
+ params.set('scope_id', options.scope_id.trim())
725
+ }
726
+
727
+ if (typeof options?.parent_id === 'string' && options.parent_id.trim().length > 0) {
728
+ params.set('parent_id', options.parent_id.trim())
729
+ }
730
+
731
+ return params.toString()
732
+ }
733
+
734
+ async function loadSpaces(options?: LoadSpacesOptions): Promise<unknown> {
679
735
  spacesLoadingState.setLoading(true)
680
- loading.value = true
736
+ beginLoading()
681
737
  error.value = null
682
738
 
683
739
  try {
684
- const response = await client.get<unknown>('/v1/discussion/spaces?tree=1')
740
+ const queryString = buildSpaceQueryString(options)
741
+ const response = await client.get<unknown>(`/v1/discussion/spaces?${queryString}`)
685
742
  const responseData = toRecord(response.data)
686
743
  const treeData = Array.isArray(responseData?.items)
687
744
  ? responseData.items as Space[]
@@ -691,10 +748,7 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
691
748
 
692
749
  spaceTree.value = treeData
693
750
  spaces.value = flattenTree(treeData)
694
-
695
- if (treeData.length === 0) {
696
- spacesLoadingState.setEmpty(true)
697
- }
751
+ spacesLoadingState.setEmpty(treeData.length === 0)
698
752
 
699
753
  return response.data
700
754
  } catch (err) {
@@ -705,7 +759,66 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
705
759
  throw err
706
760
  } finally {
707
761
  spacesLoadingState.setLoading(false)
708
- loading.value = false
762
+ endLoading()
763
+ }
764
+ }
765
+
766
+ async function createSpace(input: CreateSpaceInput): Promise<unknown> {
767
+ beginLoading()
768
+ error.value = null
769
+
770
+ try {
771
+ const response = await client.post<unknown>('/v1/discussion/spaces', {
772
+ ...input,
773
+ ...(typeof input.parent_id === 'string' ? { parent_id: input.parent_id } : { parent_id: null }),
774
+ ...(typeof input.description === 'string' && input.description.trim().length > 0
775
+ ? { description: input.description.trim() }
776
+ : {}),
777
+ ...(input.meta ? { meta: input.meta } : {}),
778
+ })
779
+
780
+ return response.data
781
+ } catch (err) {
782
+ logger.error('Failed to create discussion space:', err)
783
+ error.value = getErrorMessage(err, 'Failed to create discussion space')
784
+ throw err
785
+ } finally {
786
+ endLoading()
787
+ }
788
+ }
789
+
790
+ async function moveSpace(spaceId: string, parentId?: string | null): Promise<unknown> {
791
+ beginLoading()
792
+ error.value = null
793
+
794
+ try {
795
+ const response = await client.post<unknown>(`/v1/discussion/spaces/${spaceId}/move`, {
796
+ parent_id: parentId ?? null,
797
+ })
798
+
799
+ return response.data
800
+ } catch (err) {
801
+ logger.error('Failed to move discussion space:', err)
802
+ error.value = getErrorMessage(err, 'Failed to move discussion space')
803
+ throw err
804
+ } finally {
805
+ endLoading()
806
+ }
807
+ }
808
+
809
+ async function reorderSpaces(ids: string[]): Promise<unknown> {
810
+ beginLoading()
811
+ error.value = null
812
+
813
+ try {
814
+ const response = await client.post<unknown>('/v1/discussion/spaces/reorder', { ids })
815
+ return response.data
816
+ } catch (err) {
817
+ logger.error('Failed to reorder discussion spaces:', err)
818
+ error.value = getErrorMessage(err, 'Failed to reorder discussion spaces')
819
+ throw err
820
+ } finally {
821
+ endLoading()
709
822
  }
710
823
  }
711
824
 
@@ -813,18 +926,16 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
813
926
  async function loadThreads(
814
927
  spaceSlug: string,
815
928
  cursor?: string,
816
- options?: { signal?: AbortSignal; sort?: SpaceThreadSort }
929
+ options?: { signal?: AbortSignal; sort?: SpaceThreadSort; filter?: SpaceThreadFilter }
817
930
  ): Promise<unknown> {
818
931
  if (!cursor || latestSpaceSlug.value === null) {
819
932
  latestSpaceSlug.value = spaceSlug
820
933
  }
821
934
 
822
935
  threadsLoadingState.setLoading(true)
823
- loading.value = true
936
+ beginLoading()
824
937
  error.value = null
825
938
 
826
- let isStale = false
827
-
828
939
  try {
829
940
  const queryParams = new URLSearchParams()
830
941
  if (cursor) {
@@ -833,6 +944,9 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
833
944
  if (options?.sort) {
834
945
  queryParams.set('sort', options.sort)
835
946
  }
947
+ if (options?.filter && options.filter !== 'all') {
948
+ queryParams.set('filter', options.filter)
949
+ }
836
950
 
837
951
  const queryString = queryParams.toString()
838
952
  const url = `/v1/discussion/spaces/${spaceSlug}/threads${queryString.length > 0 ? `?${queryString}` : ''}`
@@ -853,7 +967,6 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
853
967
  }
854
968
 
855
969
  if (latestSpaceSlug.value !== spaceSlug) {
856
- isStale = true
857
970
  return response.data
858
971
  }
859
972
 
@@ -884,7 +997,6 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
884
997
  return response.data
885
998
  } catch (err) {
886
999
  if (options?.signal?.aborted || latestSpaceSlug.value !== spaceSlug) {
887
- isStale = true
888
1000
  return undefined
889
1001
  }
890
1002
 
@@ -909,10 +1021,81 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
909
1021
  error.value = errorMessage
910
1022
  throw err
911
1023
  } finally {
912
- if (!isStale) {
913
- threadsLoadingState.setLoading(false)
914
- loading.value = false
1024
+ threadsLoadingState.setLoading(false)
1025
+ endLoading()
1026
+ }
1027
+ }
1028
+
1029
+ async function browseThreads(
1030
+ view: DiscussionBrowseMode,
1031
+ cursor?: string,
1032
+ options?: { signal?: AbortSignal }
1033
+ ): Promise<unknown> {
1034
+ if (!cursor || currentBrowseMode.value !== view) {
1035
+ currentBrowseMode.value = view
1036
+ }
1037
+
1038
+ threadsLoadingState.setLoading(true)
1039
+ beginLoading()
1040
+ error.value = null
1041
+
1042
+ try {
1043
+ const queryParams = new URLSearchParams()
1044
+ if (cursor) {
1045
+ queryParams.set('cursor', cursor)
915
1046
  }
1047
+
1048
+ const queryString = queryParams.toString()
1049
+ const url = `/v1/discussion/threads/${view}${queryString.length > 0 ? `?${queryString}` : ''}`
1050
+ const requestConfig = {
1051
+ ...(options?.signal ? { signal: options.signal } : {}),
1052
+ }
1053
+
1054
+ let response: Awaited<ReturnType<typeof client.get<unknown>>>
1055
+
1056
+ try {
1057
+ response = await client.get<unknown>(url, requestConfig)
1058
+ } catch (requestError) {
1059
+ if (!isTransientThreadListError(requestError) || options?.signal?.aborted) {
1060
+ throw requestError
1061
+ }
1062
+
1063
+ response = await client.get<unknown>(url, requestConfig)
1064
+ }
1065
+
1066
+ if (currentBrowseMode.value !== view) {
1067
+ return response.data
1068
+ }
1069
+
1070
+ const newThreads = getThreadsFromPayload(response.data).map((thread) => normalizeThreadReplyCount(thread))
1071
+
1072
+ if (cursor) {
1073
+ browseThreadList.value = mergeUniqueById(browseThreadList.value, newThreads)
1074
+ } else {
1075
+ browseThreadList.value = newThreads
1076
+
1077
+ if (newThreads.length === 0) {
1078
+ threadsLoadingState.setEmpty(true)
1079
+ }
1080
+ }
1081
+
1082
+ browseNextCursor.value = getThreadNextCursorFromPayload(response.data)
1083
+ currentSpace.value = null
1084
+
1085
+ return response.data
1086
+ } catch (err) {
1087
+ if (options?.signal?.aborted || currentBrowseMode.value !== view) {
1088
+ return undefined
1089
+ }
1090
+
1091
+ logger.error('Failed to browse discussion threads:', err)
1092
+ const errorMessage = getErrorMessage(err, 'Failed to load discussion threads')
1093
+ threadsLoadingState.setError(new Error(errorMessage))
1094
+ error.value = errorMessage
1095
+ throw err
1096
+ } finally {
1097
+ threadsLoadingState.setLoading(false)
1098
+ endLoading()
916
1099
  }
917
1100
  }
918
1101
 
@@ -932,28 +1115,29 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
932
1115
  latestThreadId.value = threadId
933
1116
 
934
1117
  cleanupRealtimeChannels()
935
- loading.value = true
1118
+ beginLoading()
936
1119
  error.value = null
1120
+ replies.value = []
1121
+ repliesNextCursor.value = null
1122
+ currentReplySort.value = 'best'
937
1123
 
938
1124
  if (!threadId) {
939
1125
  error.value = 'Missing thread identifier'
940
- loading.value = false
1126
+ endLoading()
941
1127
  return null
942
1128
  }
943
1129
 
944
- if (!isValidUUID(threadId)) {
1130
+ // Accept both UUID and slug identifiers — the backend resolves by ID or slug
1131
+ if (!/^[\w-]+$/u.test(threadId)) {
945
1132
  error.value = 'Invalid thread identifier format'
946
- loading.value = false
1133
+ endLoading()
947
1134
  return null
948
1135
  }
949
1136
 
950
- let isStale = false
951
-
952
1137
  try {
953
1138
  const response = await client.get<unknown>(`/v1/discussion/threads/${threadId}`)
954
1139
 
955
1140
  if (latestThreadId.value !== threadId) {
956
- isStale = true
957
1141
  return null
958
1142
  }
959
1143
 
@@ -1010,7 +1194,6 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1010
1194
  return currentThread.value
1011
1195
  } catch (err) {
1012
1196
  if (latestThreadId.value !== threadId) {
1013
- isStale = true
1014
1197
  return null
1015
1198
  }
1016
1199
 
@@ -1036,9 +1219,7 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1036
1219
 
1037
1220
  throw err
1038
1221
  } finally {
1039
- if (!isStale) {
1040
- loading.value = false
1041
- }
1222
+ endLoading()
1042
1223
  }
1043
1224
  }
1044
1225
 
@@ -1051,11 +1232,9 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1051
1232
  latestThreadId.value = threadId
1052
1233
  }
1053
1234
 
1054
- loading.value = true
1235
+ beginLoading()
1055
1236
  error.value = null
1056
1237
 
1057
- let isStale = false
1058
-
1059
1238
  try {
1060
1239
  const queryParts: string[] = []
1061
1240
  if (cursor) {
@@ -1069,7 +1248,6 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1069
1248
  const response = await client.get<unknown>(`/v1/discussion/threads/${threadId}/replies${queryString}`)
1070
1249
 
1071
1250
  if (latestThreadId.value !== threadId) {
1072
- isStale = true
1073
1251
  return undefined
1074
1252
  }
1075
1253
 
@@ -1138,7 +1316,6 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1138
1316
  return response.data
1139
1317
  } catch (err) {
1140
1318
  if (latestThreadId.value !== threadId) {
1141
- isStale = true
1142
1319
  return undefined
1143
1320
  }
1144
1321
 
@@ -1146,9 +1323,7 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1146
1323
  error.value = getErrorMessage(err, 'Failed to load replies')
1147
1324
  throw err
1148
1325
  } finally {
1149
- if (!isStale) {
1150
- loading.value = false
1151
- }
1326
+ endLoading()
1152
1327
  }
1153
1328
  }
1154
1329
 
@@ -1167,10 +1342,19 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1167
1342
  if (existingReplyIndex !== -1) {
1168
1343
  const existingReply = replies.value[existingReplyIndex]
1169
1344
  if (existingReply) {
1170
- replies.value[existingReplyIndex] = {
1345
+ const merged = {
1171
1346
  ...existingReply,
1172
1347
  ...reply,
1173
1348
  }
1349
+
1350
+ // Broadcast payloads lack an authenticated user context, so
1351
+ // is_author is always false in them. Preserve the value from
1352
+ // the original authenticated API response when it was true.
1353
+ if (hasReplyAuthorFlag(existingReply) && !hasReplyAuthorFlag(reply)) {
1354
+ setReplyAuthorFlag(merged, true)
1355
+ }
1356
+
1357
+ replies.value[existingReplyIndex] = merged
1174
1358
  }
1175
1359
 
1176
1360
  return false
@@ -1221,7 +1405,7 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1221
1405
  input: CreateThreadInput
1222
1406
  ): Promise<DiscussionCreateThreadResponse> {
1223
1407
  cleanupRealtimeChannels()
1224
- loading.value = true
1408
+ beginLoading()
1225
1409
  error.value = null
1226
1410
 
1227
1411
  try {
@@ -1252,12 +1436,12 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1252
1436
  error.value = getErrorMessage(err, 'Failed to create thread')
1253
1437
  throw err
1254
1438
  } finally {
1255
- loading.value = false
1439
+ endLoading()
1256
1440
  }
1257
1441
  }
1258
1442
 
1259
1443
  async function loadTagCategories(query = ''): Promise<DiscussionTag[]> {
1260
- loading.value = true
1444
+ beginLoading()
1261
1445
  error.value = null
1262
1446
 
1263
1447
  try {
@@ -1292,7 +1476,7 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1292
1476
  error.value = getErrorMessage(err, 'Failed to load tag suggestions')
1293
1477
  throw err
1294
1478
  } finally {
1295
- loading.value = false
1479
+ endLoading()
1296
1480
  }
1297
1481
  }
1298
1482
 
@@ -1596,13 +1780,19 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1596
1780
  return
1597
1781
  }
1598
1782
 
1599
- replies.value[replyIndex] = normalizeReplyPayload(
1600
- {
1601
- ...existingReply,
1602
- ...payload,
1603
- },
1604
- existingReply
1605
- )
1783
+ const merged = {
1784
+ ...existingReply,
1785
+ ...payload,
1786
+ }
1787
+
1788
+ // Broadcast payloads lack an authenticated user context, so
1789
+ // is_author is always false in them. Preserve the value from
1790
+ // the original authenticated API response when it was true.
1791
+ if (hasReplyAuthorFlag(existingReply) && !hasReplyAuthorFlag(payload)) {
1792
+ setReplyAuthorFlag(merged, true)
1793
+ }
1794
+
1795
+ replies.value[replyIndex] = normalizeReplyPayload(merged, existingReply)
1606
1796
  }
1607
1797
 
1608
1798
  function handleRealtimeReplyReaction(payload: ReplyReactionRealtimePayload | null | undefined): void {
@@ -1740,8 +1930,14 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1740
1930
  }
1741
1931
  }
1742
1932
 
1743
- async function searchThreads(query: string, spaceSlug?: string): Promise<unknown> {
1744
- loading.value = true
1933
+ async function searchThreads(
1934
+ query: string,
1935
+ spaceSlug?: string,
1936
+ options?: {
1937
+ signal?: AbortSignal
1938
+ },
1939
+ ): Promise<unknown> {
1940
+ beginLoading()
1745
1941
  error.value = null
1746
1942
 
1747
1943
  try {
@@ -1750,33 +1946,49 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1750
1946
  params.append('space', spaceSlug)
1751
1947
  }
1752
1948
 
1753
- const response = await client.get<unknown>(`/v1/discussion/search/threads?${params.toString()}`)
1949
+ const requestConfig = options?.signal
1950
+ ? { signal: options.signal }
1951
+ : undefined
1952
+ const response = await client.get<unknown>(`/v1/discussion/search/threads?${params.toString()}`, requestConfig)
1754
1953
  return response.data
1755
1954
  } catch (err) {
1955
+ if (options?.signal?.aborted) {
1956
+ return undefined
1957
+ }
1958
+
1756
1959
  logger.error('Failed to search threads:', err)
1757
1960
  error.value = getErrorMessage(err, 'Failed to search')
1758
1961
  throw err
1759
1962
  } finally {
1760
- loading.value = false
1963
+ endLoading()
1761
1964
  }
1762
1965
  }
1763
1966
 
1764
- async function searchThreadsInSpace(query: string, spaceId: string): Promise<Thread[]> {
1967
+ async function searchThreadsInSpace(
1968
+ query: string,
1969
+ spaceId: string,
1970
+ options?: {
1971
+ signal?: AbortSignal
1972
+ },
1973
+ ): Promise<Thread[]> {
1765
1974
  const normalizedQuery = query.trim()
1766
1975
  if (normalizedQuery.length < 2) {
1767
1976
  return []
1768
1977
  }
1769
1978
 
1770
- loading.value = true
1979
+ beginLoading()
1771
1980
  error.value = null
1772
1981
 
1773
1982
  try {
1983
+ const requestConfig = options?.signal
1984
+ ? { signal: options.signal }
1985
+ : undefined
1774
1986
  const response = await client.post<ThreadSearchResponseBody>('/v1/discussion/search', {
1775
1987
  query: normalizedQuery,
1776
1988
  space_id: spaceId,
1777
1989
  sort_by: 'relevance',
1778
1990
  limit: 50,
1779
- })
1991
+ }, requestConfig)
1780
1992
 
1781
1993
  const rows = extractThreadSearchRows(response.data)
1782
1994
  if (rows.length > 0) {
@@ -1785,41 +1997,57 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1785
1997
 
1786
1998
  return filterLoadedThreadsByQuery(normalizedQuery, spaceId)
1787
1999
  } catch (err) {
2000
+ if (options?.signal?.aborted) {
2001
+ return []
2002
+ }
2003
+
1788
2004
  logger.error('Failed to search threads in space:', err)
1789
2005
  error.value = isAxiosError(err)
1790
2006
  ? err.response?.data?.message ?? 'Failed to search threads'
1791
2007
  : 'Failed to search threads'
1792
2008
  throw err
1793
2009
  } finally {
1794
- loading.value = false
2010
+ endLoading()
1795
2011
  }
1796
2012
  }
1797
2013
 
1798
- async function searchThreadsGlobally(query: string): Promise<Thread[]> {
2014
+ async function searchThreadsGlobally(
2015
+ query: string,
2016
+ options?: {
2017
+ signal?: AbortSignal
2018
+ },
2019
+ ): Promise<Thread[]> {
1799
2020
  const normalizedQuery = query.trim()
1800
2021
  if (normalizedQuery.length < 2) {
1801
2022
  return []
1802
2023
  }
1803
2024
 
1804
- loading.value = true
2025
+ beginLoading()
1805
2026
  error.value = null
1806
2027
 
1807
2028
  try {
2029
+ const requestConfig = options?.signal
2030
+ ? { signal: options.signal }
2031
+ : undefined
1808
2032
  const response = await client.post<ThreadSearchResponseBody>('/v1/discussion/search', {
1809
2033
  query: normalizedQuery,
1810
2034
  sort_by: 'relevance',
1811
2035
  limit: 50,
1812
- })
2036
+ }, requestConfig)
1813
2037
 
1814
2038
  return extractThreadSearchRows(response.data).map((row) => mapSearchRowToThread(row))
1815
2039
  } catch (err) {
2040
+ if (options?.signal?.aborted) {
2041
+ return []
2042
+ }
2043
+
1816
2044
  logger.error('Failed to search threads globally:', err)
1817
2045
  error.value = isAxiosError(err)
1818
2046
  ? err.response?.data?.message ?? 'Failed to search threads'
1819
2047
  : 'Failed to search threads'
1820
2048
  throw err
1821
2049
  } finally {
1822
- loading.value = false
2050
+ endLoading()
1823
2051
  }
1824
2052
  }
1825
2053
 
@@ -1851,6 +2079,15 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1851
2079
  thread.slug = row.slug
1852
2080
  }
1853
2081
 
2082
+ const spaceId = row.space_id ?? fallbackSpaceId ?? ''
2083
+ if (typeof row.space_name === 'string' && typeof row.space_slug === 'string' && spaceId) {
2084
+ thread.space = {
2085
+ id: spaceId,
2086
+ slug: row.space_slug,
2087
+ name: row.space_name,
2088
+ }
2089
+ }
2090
+
1854
2091
  return thread
1855
2092
  }
1856
2093
 
@@ -1896,7 +2133,7 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1896
2133
  }
1897
2134
 
1898
2135
  async function setThreadPinned(threadId: string, pinned: boolean): Promise<void> {
1899
- loading.value = true
2136
+ beginLoading()
1900
2137
  error.value = null
1901
2138
 
1902
2139
  try {
@@ -1917,12 +2154,12 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1917
2154
  error.value = getErrorMessage(err, 'Failed to update pinned status')
1918
2155
  throw err
1919
2156
  } finally {
1920
- loading.value = false
2157
+ endLoading()
1921
2158
  }
1922
2159
  }
1923
2160
 
1924
2161
  async function setThreadLocked(threadId: string, locked: boolean): Promise<void> {
1925
- loading.value = true
2162
+ beginLoading()
1926
2163
  error.value = null
1927
2164
 
1928
2165
  try {
@@ -1942,12 +2179,12 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1942
2179
  error.value = getErrorMessage(err, 'Failed to update locked status')
1943
2180
  throw err
1944
2181
  } finally {
1945
- loading.value = false
2182
+ endLoading()
1946
2183
  }
1947
2184
  }
1948
2185
 
1949
2186
  async function moveThread(threadId: string, toSpaceSlug: string): Promise<void> {
1950
- loading.value = true
2187
+ beginLoading()
1951
2188
  error.value = null
1952
2189
 
1953
2190
  try {
@@ -1958,12 +2195,22 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1958
2195
  const responseData = toRecord(response.data)
1959
2196
  const movedThread = (responseData?.data ?? null) as Thread | null
1960
2197
  const destinationSpace = spaces.value.find((space) => space.slug === toSpaceSlug)
2198
+ const destinationSpaceSummary = destinationSpace
2199
+ ? {
2200
+ id: destinationSpace.id,
2201
+ slug: destinationSpace.slug,
2202
+ name: destinationSpace.name,
2203
+ }
2204
+ : null
1961
2205
 
1962
2206
  if (currentThread.value?.id === threadId && currentThread.value) {
1963
2207
  currentThread.value = {
1964
2208
  ...currentThread.value,
1965
2209
  ...(movedThread ?? {}),
1966
- ...(destinationSpace ? { space_id: destinationSpace.id } : {}),
2210
+ ...(destinationSpaceSummary ? {
2211
+ space_id: destinationSpaceSummary.id,
2212
+ space: destinationSpaceSummary,
2213
+ } : {}),
1967
2214
  }
1968
2215
  }
1969
2216
 
@@ -1981,7 +2228,10 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1981
2228
  threads.value[threadIndex] = {
1982
2229
  ...existingThread,
1983
2230
  ...(movedThread ?? {}),
1984
- ...(destinationSpace ? { space_id: destinationSpace.id } : {}),
2231
+ ...(destinationSpaceSummary ? {
2232
+ space_id: destinationSpaceSummary.id,
2233
+ space: destinationSpaceSummary,
2234
+ } : {}),
1985
2235
  }
1986
2236
  }
1987
2237
  }
@@ -1992,12 +2242,12 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
1992
2242
  error.value = getErrorMessage(err, 'Failed to move thread')
1993
2243
  throw err
1994
2244
  } finally {
1995
- loading.value = false
2245
+ endLoading()
1996
2246
  }
1997
2247
  }
1998
2248
 
1999
2249
  async function updateThread(threadId: string, updates: UpdateThreadInput): Promise<unknown> {
2000
- loading.value = true
2250
+ beginLoading()
2001
2251
  error.value = null
2002
2252
 
2003
2253
  try {
@@ -2030,12 +2280,12 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
2030
2280
  error.value = getErrorMessage(err, 'Failed to update thread')
2031
2281
  throw err
2032
2282
  } finally {
2033
- loading.value = false
2283
+ endLoading()
2034
2284
  }
2035
2285
  }
2036
2286
 
2037
2287
  async function deleteThread(threadId: string): Promise<unknown> {
2038
- loading.value = true
2288
+ beginLoading()
2039
2289
  error.value = null
2040
2290
 
2041
2291
  try {
@@ -2053,12 +2303,12 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
2053
2303
  error.value = getErrorMessage(err, 'Failed to delete thread')
2054
2304
  throw err
2055
2305
  } finally {
2056
- loading.value = false
2306
+ endLoading()
2057
2307
  }
2058
2308
  }
2059
2309
 
2060
2310
  async function restoreThread(threadId: string): Promise<Thread> {
2061
- loading.value = true
2311
+ beginLoading()
2062
2312
  error.value = null
2063
2313
 
2064
2314
  try {
@@ -2101,12 +2351,12 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
2101
2351
  error.value = getErrorMessage(err, 'Failed to restore thread')
2102
2352
  throw err
2103
2353
  } finally {
2104
- loading.value = false
2354
+ endLoading()
2105
2355
  }
2106
2356
  }
2107
2357
 
2108
2358
  async function deleteReply(replyId: string): Promise<void> {
2109
- loading.value = true
2359
+ beginLoading()
2110
2360
  error.value = null
2111
2361
 
2112
2362
  const replyIndex = replies.value.findIndex((reply) => reply.id === replyId)
@@ -2170,7 +2420,7 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
2170
2420
  error.value = getErrorMessage(err, 'Failed to delete reply')
2171
2421
  throw err
2172
2422
  } finally {
2173
- loading.value = false
2423
+ endLoading()
2174
2424
  }
2175
2425
  }
2176
2426
 
@@ -2431,16 +2681,23 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
2431
2681
  spaceMembershipLoading,
2432
2682
  cleanupRealtimeChannels,
2433
2683
  threads,
2684
+ browseThreadList,
2685
+ currentBrowseMode,
2434
2686
  currentThread,
2435
2687
  replies,
2688
+ currentReplySort,
2436
2689
  loading,
2437
2690
  error,
2438
2691
  nextCursor,
2692
+ browseNextCursor,
2439
2693
  repliesNextCursor,
2440
2694
  spacesLoadingState,
2441
2695
  threadsLoadingState,
2442
2696
  repliesLoadingState,
2443
2697
  loadSpaces,
2698
+ createSpace,
2699
+ moveSpace,
2700
+ reorderSpaces,
2444
2701
  loadSpaceDetail,
2445
2702
  loadSpaceMembership,
2446
2703
  joinSpace,
@@ -2450,6 +2707,7 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
2450
2707
  rootSpaces,
2451
2708
  leafSpaces,
2452
2709
  loadThreads,
2710
+ browseThreads,
2453
2711
  loadThread,
2454
2712
  loadReplies,
2455
2713
  createThread,