@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
@@ -124,6 +124,8 @@ export function createDiscussionStoreDefinition(config) {
124
124
  const spaceMemberships = ref({});
125
125
  const spaceMembershipLoading = ref({});
126
126
  const threads = ref([]);
127
+ const browseThreadList = ref([]);
128
+ const currentBrowseMode = ref(null);
127
129
  const currentThread = ref(null);
128
130
  const replies = ref([]);
129
131
  const currentReplySort = ref('best');
@@ -131,8 +133,10 @@ export function createDiscussionStoreDefinition(config) {
131
133
  const threadsLoadingState = createLoadingState();
132
134
  const repliesLoadingState = createLoadingState();
133
135
  const loading = ref(false);
136
+ let activeLoadingRequests = 0;
134
137
  const error = ref(null);
135
138
  const nextCursor = ref(null);
139
+ const browseNextCursor = ref(null);
136
140
  const repliesNextCursor = ref(null);
137
141
  const latestSpaceSlug = ref(null);
138
142
  const latestThreadId = ref(null);
@@ -237,6 +241,17 @@ export function createDiscussionStoreDefinition(config) {
237
241
  }
238
242
  return hasQuotedReplyBody(incomingQuotedReply) ? incomingQuotedReply : null;
239
243
  }
244
+ /**
245
+ * Check whether a reply-like object carries a truthy `is_author` flag.
246
+ * The field is not part of the Reply TS interface (it is user-specific
247
+ * metadata injected by the API) so we access it reflectively.
248
+ */
249
+ function hasReplyAuthorFlag(reply) {
250
+ return Reflect.get(reply, 'is_author') === true;
251
+ }
252
+ function setReplyAuthorFlag(reply, value) {
253
+ Reflect.set(reply, 'is_author', value);
254
+ }
240
255
  function normalizeReplyPayload(reply, existingReply) {
241
256
  const normalized = { ...reply };
242
257
  const hasQuotedReplyId = typeof normalized.quoted_reply_id === 'string'
@@ -448,6 +463,14 @@ export function createDiscussionStoreDefinition(config) {
448
463
  THREAD: 'thread',
449
464
  REPLY: 'reply',
450
465
  };
466
+ function beginLoading() {
467
+ activeLoadingRequests += 1;
468
+ loading.value = true;
469
+ }
470
+ function endLoading() {
471
+ activeLoadingRequests = Math.max(0, activeLoadingRequests - 1);
472
+ loading.value = activeLoadingRequests > 0;
473
+ }
451
474
  function flattenTree(tree) {
452
475
  const result = [];
453
476
  function walk(nodes) {
@@ -467,12 +490,30 @@ export function createDiscussionStoreDefinition(config) {
467
490
  function leafSpaces() {
468
491
  return spaces.value.filter((space) => space.is_leaf !== false);
469
492
  }
470
- async function loadSpaces() {
493
+ function buildSpaceQueryString(options) {
494
+ const params = new URLSearchParams();
495
+ params.set('tree', options?.tree === false ? '0' : '1');
496
+ if (options?.kind) {
497
+ params.set('kind', options.kind);
498
+ }
499
+ if (typeof options?.scope_type === 'string' && options.scope_type.trim().length > 0) {
500
+ params.set('scope_type', options.scope_type.trim());
501
+ }
502
+ if (typeof options?.scope_id === 'string' && options.scope_id.trim().length > 0) {
503
+ params.set('scope_id', options.scope_id.trim());
504
+ }
505
+ if (typeof options?.parent_id === 'string' && options.parent_id.trim().length > 0) {
506
+ params.set('parent_id', options.parent_id.trim());
507
+ }
508
+ return params.toString();
509
+ }
510
+ async function loadSpaces(options) {
471
511
  spacesLoadingState.setLoading(true);
472
- loading.value = true;
512
+ beginLoading();
473
513
  error.value = null;
474
514
  try {
475
- const response = await client.get('/v1/discussion/spaces?tree=1');
515
+ const queryString = buildSpaceQueryString(options);
516
+ const response = await client.get(`/v1/discussion/spaces?${queryString}`);
476
517
  const responseData = toRecord(response.data);
477
518
  const treeData = Array.isArray(responseData?.items)
478
519
  ? responseData.items
@@ -481,9 +522,7 @@ export function createDiscussionStoreDefinition(config) {
481
522
  : [];
482
523
  spaceTree.value = treeData;
483
524
  spaces.value = flattenTree(treeData);
484
- if (treeData.length === 0) {
485
- spacesLoadingState.setEmpty(true);
486
- }
525
+ spacesLoadingState.setEmpty(treeData.length === 0);
487
526
  return response.data;
488
527
  }
489
528
  catch (err) {
@@ -495,7 +534,64 @@ export function createDiscussionStoreDefinition(config) {
495
534
  }
496
535
  finally {
497
536
  spacesLoadingState.setLoading(false);
498
- loading.value = false;
537
+ endLoading();
538
+ }
539
+ }
540
+ async function createSpace(input) {
541
+ beginLoading();
542
+ error.value = null;
543
+ try {
544
+ const response = await client.post('/v1/discussion/spaces', {
545
+ ...input,
546
+ ...(typeof input.parent_id === 'string' ? { parent_id: input.parent_id } : { parent_id: null }),
547
+ ...(typeof input.description === 'string' && input.description.trim().length > 0
548
+ ? { description: input.description.trim() }
549
+ : {}),
550
+ ...(input.meta ? { meta: input.meta } : {}),
551
+ });
552
+ return response.data;
553
+ }
554
+ catch (err) {
555
+ logger.error('Failed to create discussion space:', err);
556
+ error.value = getErrorMessage(err, 'Failed to create discussion space');
557
+ throw err;
558
+ }
559
+ finally {
560
+ endLoading();
561
+ }
562
+ }
563
+ async function moveSpace(spaceId, parentId) {
564
+ beginLoading();
565
+ error.value = null;
566
+ try {
567
+ const response = await client.post(`/v1/discussion/spaces/${spaceId}/move`, {
568
+ parent_id: parentId ?? null,
569
+ });
570
+ return response.data;
571
+ }
572
+ catch (err) {
573
+ logger.error('Failed to move discussion space:', err);
574
+ error.value = getErrorMessage(err, 'Failed to move discussion space');
575
+ throw err;
576
+ }
577
+ finally {
578
+ endLoading();
579
+ }
580
+ }
581
+ async function reorderSpaces(ids) {
582
+ beginLoading();
583
+ error.value = null;
584
+ try {
585
+ const response = await client.post('/v1/discussion/spaces/reorder', { ids });
586
+ return response.data;
587
+ }
588
+ catch (err) {
589
+ logger.error('Failed to reorder discussion spaces:', err);
590
+ error.value = getErrorMessage(err, 'Failed to reorder discussion spaces');
591
+ throw err;
592
+ }
593
+ finally {
594
+ endLoading();
499
595
  }
500
596
  }
501
597
  async function loadSpaceDetail(slug, options) {
@@ -577,9 +673,8 @@ export function createDiscussionStoreDefinition(config) {
577
673
  latestSpaceSlug.value = spaceSlug;
578
674
  }
579
675
  threadsLoadingState.setLoading(true);
580
- loading.value = true;
676
+ beginLoading();
581
677
  error.value = null;
582
- let isStale = false;
583
678
  try {
584
679
  const queryParams = new URLSearchParams();
585
680
  if (cursor) {
@@ -588,6 +683,9 @@ export function createDiscussionStoreDefinition(config) {
588
683
  if (options?.sort) {
589
684
  queryParams.set('sort', options.sort);
590
685
  }
686
+ if (options?.filter && options.filter !== 'all') {
687
+ queryParams.set('filter', options.filter);
688
+ }
591
689
  const queryString = queryParams.toString();
592
690
  const url = `/v1/discussion/spaces/${spaceSlug}/threads${queryString.length > 0 ? `?${queryString}` : ''}`;
593
691
  const requestConfig = {
@@ -604,7 +702,6 @@ export function createDiscussionStoreDefinition(config) {
604
702
  response = await client.get(url, requestConfig);
605
703
  }
606
704
  if (latestSpaceSlug.value !== spaceSlug) {
607
- isStale = true;
608
705
  return response.data;
609
706
  }
610
707
  const newThreads = getThreadsFromPayload(response.data).map((thread) => normalizeThreadReplyCount(thread));
@@ -630,7 +727,6 @@ export function createDiscussionStoreDefinition(config) {
630
727
  }
631
728
  catch (err) {
632
729
  if (options?.signal?.aborted || latestSpaceSlug.value !== spaceSlug) {
633
- isStale = true;
634
730
  return undefined;
635
731
  }
636
732
  if (isAxiosError(err) && err.response?.status === 404) {
@@ -654,10 +750,67 @@ export function createDiscussionStoreDefinition(config) {
654
750
  throw err;
655
751
  }
656
752
  finally {
657
- if (!isStale) {
658
- threadsLoadingState.setLoading(false);
659
- loading.value = false;
753
+ threadsLoadingState.setLoading(false);
754
+ endLoading();
755
+ }
756
+ }
757
+ async function browseThreads(view, cursor, options) {
758
+ if (!cursor || currentBrowseMode.value !== view) {
759
+ currentBrowseMode.value = view;
760
+ }
761
+ threadsLoadingState.setLoading(true);
762
+ beginLoading();
763
+ error.value = null;
764
+ try {
765
+ const queryParams = new URLSearchParams();
766
+ if (cursor) {
767
+ queryParams.set('cursor', cursor);
660
768
  }
769
+ const queryString = queryParams.toString();
770
+ const url = `/v1/discussion/threads/${view}${queryString.length > 0 ? `?${queryString}` : ''}`;
771
+ const requestConfig = {
772
+ ...(options?.signal ? { signal: options.signal } : {}),
773
+ };
774
+ let response;
775
+ try {
776
+ response = await client.get(url, requestConfig);
777
+ }
778
+ catch (requestError) {
779
+ if (!isTransientThreadListError(requestError) || options?.signal?.aborted) {
780
+ throw requestError;
781
+ }
782
+ response = await client.get(url, requestConfig);
783
+ }
784
+ if (currentBrowseMode.value !== view) {
785
+ return response.data;
786
+ }
787
+ const newThreads = getThreadsFromPayload(response.data).map((thread) => normalizeThreadReplyCount(thread));
788
+ if (cursor) {
789
+ browseThreadList.value = mergeUniqueById(browseThreadList.value, newThreads);
790
+ }
791
+ else {
792
+ browseThreadList.value = newThreads;
793
+ if (newThreads.length === 0) {
794
+ threadsLoadingState.setEmpty(true);
795
+ }
796
+ }
797
+ browseNextCursor.value = getThreadNextCursorFromPayload(response.data);
798
+ currentSpace.value = null;
799
+ return response.data;
800
+ }
801
+ catch (err) {
802
+ if (options?.signal?.aborted || currentBrowseMode.value !== view) {
803
+ return undefined;
804
+ }
805
+ logger.error('Failed to browse discussion threads:', err);
806
+ const errorMessage = getErrorMessage(err, 'Failed to load discussion threads');
807
+ threadsLoadingState.setError(new Error(errorMessage));
808
+ error.value = errorMessage;
809
+ throw err;
810
+ }
811
+ finally {
812
+ threadsLoadingState.setLoading(false);
813
+ endLoading();
661
814
  }
662
815
  }
663
816
  function isValidUUID(uuid) {
@@ -672,23 +825,25 @@ export function createDiscussionStoreDefinition(config) {
672
825
  async function loadThread(spaceSlug, threadId) {
673
826
  latestThreadId.value = threadId;
674
827
  cleanupRealtimeChannels();
675
- loading.value = true;
828
+ beginLoading();
676
829
  error.value = null;
830
+ replies.value = [];
831
+ repliesNextCursor.value = null;
832
+ currentReplySort.value = 'best';
677
833
  if (!threadId) {
678
834
  error.value = 'Missing thread identifier';
679
- loading.value = false;
835
+ endLoading();
680
836
  return null;
681
837
  }
682
- if (!isValidUUID(threadId)) {
838
+ // Accept both UUID and slug identifiers — the backend resolves by ID or slug
839
+ if (!/^[\w-]+$/u.test(threadId)) {
683
840
  error.value = 'Invalid thread identifier format';
684
- loading.value = false;
841
+ endLoading();
685
842
  return null;
686
843
  }
687
- let isStale = false;
688
844
  try {
689
845
  const response = await client.get(`/v1/discussion/threads/${threadId}`);
690
846
  if (latestThreadId.value !== threadId) {
691
- isStale = true;
692
847
  return null;
693
848
  }
694
849
  const responseData = toRecord(response.data);
@@ -738,7 +893,6 @@ export function createDiscussionStoreDefinition(config) {
738
893
  }
739
894
  catch (err) {
740
895
  if (latestThreadId.value !== threadId) {
741
- isStale = true;
742
896
  return null;
743
897
  }
744
898
  const errorResponseData = getErrorResponseData(err);
@@ -764,18 +918,15 @@ export function createDiscussionStoreDefinition(config) {
764
918
  throw err;
765
919
  }
766
920
  finally {
767
- if (!isStale) {
768
- loading.value = false;
769
- }
921
+ endLoading();
770
922
  }
771
923
  }
772
924
  async function loadReplies(threadId, cursor, sortBy = 'best') {
773
925
  if (latestThreadId.value === null) {
774
926
  latestThreadId.value = threadId;
775
927
  }
776
- loading.value = true;
928
+ beginLoading();
777
929
  error.value = null;
778
- let isStale = false;
779
930
  try {
780
931
  const queryParts = [];
781
932
  if (cursor) {
@@ -787,7 +938,6 @@ export function createDiscussionStoreDefinition(config) {
787
938
  const queryString = queryParts.length > 0 ? `?${queryParts.join('&')}` : '';
788
939
  const response = await client.get(`/v1/discussion/threads/${threadId}/replies${queryString}`);
789
940
  if (latestThreadId.value !== threadId) {
790
- isStale = true;
791
941
  return undefined;
792
942
  }
793
943
  const responseData = toRecord(response.data);
@@ -847,7 +997,6 @@ export function createDiscussionStoreDefinition(config) {
847
997
  }
848
998
  catch (err) {
849
999
  if (latestThreadId.value !== threadId) {
850
- isStale = true;
851
1000
  return undefined;
852
1001
  }
853
1002
  logger.error('Failed to load replies:', err);
@@ -855,9 +1004,7 @@ export function createDiscussionStoreDefinition(config) {
855
1004
  throw err;
856
1005
  }
857
1006
  finally {
858
- if (!isStale) {
859
- loading.value = false;
860
- }
1007
+ endLoading();
861
1008
  }
862
1009
  }
863
1010
  function insertTopLevelReply(reply) {
@@ -872,10 +1019,17 @@ export function createDiscussionStoreDefinition(config) {
872
1019
  if (existingReplyIndex !== -1) {
873
1020
  const existingReply = replies.value[existingReplyIndex];
874
1021
  if (existingReply) {
875
- replies.value[existingReplyIndex] = {
1022
+ const merged = {
876
1023
  ...existingReply,
877
1024
  ...reply,
878
1025
  };
1026
+ // Broadcast payloads lack an authenticated user context, so
1027
+ // is_author is always false in them. Preserve the value from
1028
+ // the original authenticated API response when it was true.
1029
+ if (hasReplyAuthorFlag(existingReply) && !hasReplyAuthorFlag(reply)) {
1030
+ setReplyAuthorFlag(merged, true);
1031
+ }
1032
+ replies.value[existingReplyIndex] = merged;
879
1033
  }
880
1034
  return false;
881
1035
  }
@@ -913,7 +1067,7 @@ export function createDiscussionStoreDefinition(config) {
913
1067
  }
914
1068
  async function createThread(spaceSlug, input) {
915
1069
  cleanupRealtimeChannels();
916
- loading.value = true;
1070
+ beginLoading();
917
1071
  error.value = null;
918
1072
  try {
919
1073
  const response = await client.post(`/v1/discussion/spaces/${spaceSlug}/threads`, {
@@ -941,11 +1095,11 @@ export function createDiscussionStoreDefinition(config) {
941
1095
  throw err;
942
1096
  }
943
1097
  finally {
944
- loading.value = false;
1098
+ endLoading();
945
1099
  }
946
1100
  }
947
1101
  async function loadTagCategories(query = '') {
948
- loading.value = true;
1102
+ beginLoading();
949
1103
  error.value = null;
950
1104
  try {
951
1105
  const normalizedQuery = query.trim();
@@ -979,7 +1133,7 @@ export function createDiscussionStoreDefinition(config) {
979
1133
  throw err;
980
1134
  }
981
1135
  finally {
982
- loading.value = false;
1136
+ endLoading();
983
1137
  }
984
1138
  }
985
1139
  function cleanupRealtimeChannels() {
@@ -1221,10 +1375,17 @@ export function createDiscussionStoreDefinition(config) {
1221
1375
  if (!existingReply) {
1222
1376
  return;
1223
1377
  }
1224
- replies.value[replyIndex] = normalizeReplyPayload({
1378
+ const merged = {
1225
1379
  ...existingReply,
1226
1380
  ...payload,
1227
- }, existingReply);
1381
+ };
1382
+ // Broadcast payloads lack an authenticated user context, so
1383
+ // is_author is always false in them. Preserve the value from
1384
+ // the original authenticated API response when it was true.
1385
+ if (hasReplyAuthorFlag(existingReply) && !hasReplyAuthorFlag(payload)) {
1386
+ setReplyAuthorFlag(merged, true);
1387
+ }
1388
+ replies.value[replyIndex] = normalizeReplyPayload(merged, existingReply);
1228
1389
  }
1229
1390
  function handleRealtimeReplyReaction(payload) {
1230
1391
  if (!payload?.reply_id || !payload.thread_id || !payload.user_id) {
@@ -1343,40 +1504,49 @@ export function createDiscussionStoreDefinition(config) {
1343
1504
  throw err;
1344
1505
  }
1345
1506
  }
1346
- async function searchThreads(query, spaceSlug) {
1347
- loading.value = true;
1507
+ async function searchThreads(query, spaceSlug, options) {
1508
+ beginLoading();
1348
1509
  error.value = null;
1349
1510
  try {
1350
1511
  const params = new URLSearchParams({ q: query });
1351
1512
  if (spaceSlug) {
1352
1513
  params.append('space', spaceSlug);
1353
1514
  }
1354
- const response = await client.get(`/v1/discussion/search/threads?${params.toString()}`);
1515
+ const requestConfig = options?.signal
1516
+ ? { signal: options.signal }
1517
+ : undefined;
1518
+ const response = await client.get(`/v1/discussion/search/threads?${params.toString()}`, requestConfig);
1355
1519
  return response.data;
1356
1520
  }
1357
1521
  catch (err) {
1522
+ if (options?.signal?.aborted) {
1523
+ return undefined;
1524
+ }
1358
1525
  logger.error('Failed to search threads:', err);
1359
1526
  error.value = getErrorMessage(err, 'Failed to search');
1360
1527
  throw err;
1361
1528
  }
1362
1529
  finally {
1363
- loading.value = false;
1530
+ endLoading();
1364
1531
  }
1365
1532
  }
1366
- async function searchThreadsInSpace(query, spaceId) {
1533
+ async function searchThreadsInSpace(query, spaceId, options) {
1367
1534
  const normalizedQuery = query.trim();
1368
1535
  if (normalizedQuery.length < 2) {
1369
1536
  return [];
1370
1537
  }
1371
- loading.value = true;
1538
+ beginLoading();
1372
1539
  error.value = null;
1373
1540
  try {
1541
+ const requestConfig = options?.signal
1542
+ ? { signal: options.signal }
1543
+ : undefined;
1374
1544
  const response = await client.post('/v1/discussion/search', {
1375
1545
  query: normalizedQuery,
1376
1546
  space_id: spaceId,
1377
1547
  sort_by: 'relevance',
1378
1548
  limit: 50,
1379
- });
1549
+ }, requestConfig);
1380
1550
  const rows = extractThreadSearchRows(response.data);
1381
1551
  if (rows.length > 0) {
1382
1552
  return rows.map((row) => mapSearchRowToThread(row, spaceId));
@@ -1384,6 +1554,9 @@ export function createDiscussionStoreDefinition(config) {
1384
1554
  return filterLoadedThreadsByQuery(normalizedQuery, spaceId);
1385
1555
  }
1386
1556
  catch (err) {
1557
+ if (options?.signal?.aborted) {
1558
+ return [];
1559
+ }
1387
1560
  logger.error('Failed to search threads in space:', err);
1388
1561
  error.value = isAxiosError(err)
1389
1562
  ? err.response?.data?.message ?? 'Failed to search threads'
@@ -1391,25 +1564,31 @@ export function createDiscussionStoreDefinition(config) {
1391
1564
  throw err;
1392
1565
  }
1393
1566
  finally {
1394
- loading.value = false;
1567
+ endLoading();
1395
1568
  }
1396
1569
  }
1397
- async function searchThreadsGlobally(query) {
1570
+ async function searchThreadsGlobally(query, options) {
1398
1571
  const normalizedQuery = query.trim();
1399
1572
  if (normalizedQuery.length < 2) {
1400
1573
  return [];
1401
1574
  }
1402
- loading.value = true;
1575
+ beginLoading();
1403
1576
  error.value = null;
1404
1577
  try {
1578
+ const requestConfig = options?.signal
1579
+ ? { signal: options.signal }
1580
+ : undefined;
1405
1581
  const response = await client.post('/v1/discussion/search', {
1406
1582
  query: normalizedQuery,
1407
1583
  sort_by: 'relevance',
1408
1584
  limit: 50,
1409
- });
1585
+ }, requestConfig);
1410
1586
  return extractThreadSearchRows(response.data).map((row) => mapSearchRowToThread(row));
1411
1587
  }
1412
1588
  catch (err) {
1589
+ if (options?.signal?.aborted) {
1590
+ return [];
1591
+ }
1413
1592
  logger.error('Failed to search threads globally:', err);
1414
1593
  error.value = isAxiosError(err)
1415
1594
  ? err.response?.data?.message ?? 'Failed to search threads'
@@ -1417,7 +1596,7 @@ export function createDiscussionStoreDefinition(config) {
1417
1596
  throw err;
1418
1597
  }
1419
1598
  finally {
1420
- loading.value = false;
1599
+ endLoading();
1421
1600
  }
1422
1601
  }
1423
1602
  function mapSearchRowToThread(row, fallbackSpaceId) {
@@ -1445,6 +1624,14 @@ export function createDiscussionStoreDefinition(config) {
1445
1624
  if (typeof row.slug === 'string') {
1446
1625
  thread.slug = row.slug;
1447
1626
  }
1627
+ const spaceId = row.space_id ?? fallbackSpaceId ?? '';
1628
+ if (typeof row.space_name === 'string' && typeof row.space_slug === 'string' && spaceId) {
1629
+ thread.space = {
1630
+ id: spaceId,
1631
+ slug: row.space_slug,
1632
+ name: row.space_name,
1633
+ };
1634
+ }
1448
1635
  return thread;
1449
1636
  }
1450
1637
  function extractThreadSearchRows(responseBody) {
@@ -1480,7 +1667,7 @@ export function createDiscussionStoreDefinition(config) {
1480
1667
  return spaces.value.filter((space) => space.meta?.is_featured);
1481
1668
  }
1482
1669
  async function setThreadPinned(threadId, pinned) {
1483
- loading.value = true;
1670
+ beginLoading();
1484
1671
  error.value = null;
1485
1672
  try {
1486
1673
  await client.post(`/v1/discussion/threads/${threadId}/pin`, { pinned });
@@ -1499,11 +1686,11 @@ export function createDiscussionStoreDefinition(config) {
1499
1686
  throw err;
1500
1687
  }
1501
1688
  finally {
1502
- loading.value = false;
1689
+ endLoading();
1503
1690
  }
1504
1691
  }
1505
1692
  async function setThreadLocked(threadId, locked) {
1506
- loading.value = true;
1693
+ beginLoading();
1507
1694
  error.value = null;
1508
1695
  try {
1509
1696
  await client.post(`/v1/discussion/threads/${threadId}/lock`, { locked });
@@ -1522,11 +1709,11 @@ export function createDiscussionStoreDefinition(config) {
1522
1709
  throw err;
1523
1710
  }
1524
1711
  finally {
1525
- loading.value = false;
1712
+ endLoading();
1526
1713
  }
1527
1714
  }
1528
1715
  async function moveThread(threadId, toSpaceSlug) {
1529
- loading.value = true;
1716
+ beginLoading();
1530
1717
  error.value = null;
1531
1718
  try {
1532
1719
  const response = await client.post(`/v1/discussion/threads/${threadId}/move`, {
@@ -1535,11 +1722,21 @@ export function createDiscussionStoreDefinition(config) {
1535
1722
  const responseData = toRecord(response.data);
1536
1723
  const movedThread = (responseData?.data ?? null);
1537
1724
  const destinationSpace = spaces.value.find((space) => space.slug === toSpaceSlug);
1725
+ const destinationSpaceSummary = destinationSpace
1726
+ ? {
1727
+ id: destinationSpace.id,
1728
+ slug: destinationSpace.slug,
1729
+ name: destinationSpace.name,
1730
+ }
1731
+ : null;
1538
1732
  if (currentThread.value?.id === threadId && currentThread.value) {
1539
1733
  currentThread.value = {
1540
1734
  ...currentThread.value,
1541
1735
  ...(movedThread ?? {}),
1542
- ...(destinationSpace ? { space_id: destinationSpace.id } : {}),
1736
+ ...(destinationSpaceSummary ? {
1737
+ space_id: destinationSpaceSummary.id,
1738
+ space: destinationSpaceSummary,
1739
+ } : {}),
1543
1740
  };
1544
1741
  }
1545
1742
  const currentSpaceSlug = currentSpace.value?.slug ?? null;
@@ -1556,7 +1753,10 @@ export function createDiscussionStoreDefinition(config) {
1556
1753
  threads.value[threadIndex] = {
1557
1754
  ...existingThread,
1558
1755
  ...(movedThread ?? {}),
1559
- ...(destinationSpace ? { space_id: destinationSpace.id } : {}),
1756
+ ...(destinationSpaceSummary ? {
1757
+ space_id: destinationSpaceSummary.id,
1758
+ space: destinationSpaceSummary,
1759
+ } : {}),
1560
1760
  };
1561
1761
  }
1562
1762
  }
@@ -1568,11 +1768,11 @@ export function createDiscussionStoreDefinition(config) {
1568
1768
  throw err;
1569
1769
  }
1570
1770
  finally {
1571
- loading.value = false;
1771
+ endLoading();
1572
1772
  }
1573
1773
  }
1574
1774
  async function updateThread(threadId, updates) {
1575
- loading.value = true;
1775
+ beginLoading();
1576
1776
  error.value = null;
1577
1777
  try {
1578
1778
  const response = await client.patch(`/v1/discussion/threads/${threadId}`, {
@@ -1602,11 +1802,11 @@ export function createDiscussionStoreDefinition(config) {
1602
1802
  throw err;
1603
1803
  }
1604
1804
  finally {
1605
- loading.value = false;
1805
+ endLoading();
1606
1806
  }
1607
1807
  }
1608
1808
  async function deleteThread(threadId) {
1609
- loading.value = true;
1809
+ beginLoading();
1610
1810
  error.value = null;
1611
1811
  try {
1612
1812
  const response = await client.delete(`/v1/discussion/threads/${threadId}`);
@@ -1622,11 +1822,11 @@ export function createDiscussionStoreDefinition(config) {
1622
1822
  throw err;
1623
1823
  }
1624
1824
  finally {
1625
- loading.value = false;
1825
+ endLoading();
1626
1826
  }
1627
1827
  }
1628
1828
  async function restoreThread(threadId) {
1629
- loading.value = true;
1829
+ beginLoading();
1630
1830
  error.value = null;
1631
1831
  try {
1632
1832
  const response = await client.post(`/v1/discussion/threads/${threadId}/restore`);
@@ -1667,11 +1867,11 @@ export function createDiscussionStoreDefinition(config) {
1667
1867
  throw err;
1668
1868
  }
1669
1869
  finally {
1670
- loading.value = false;
1870
+ endLoading();
1671
1871
  }
1672
1872
  }
1673
1873
  async function deleteReply(replyId) {
1674
- loading.value = true;
1874
+ beginLoading();
1675
1875
  error.value = null;
1676
1876
  const replyIndex = replies.value.findIndex((reply) => reply.id === replyId);
1677
1877
  const replyToRestore = replyIndex >= 0 ? replies.value[replyIndex] ?? null : null;
@@ -1729,7 +1929,7 @@ export function createDiscussionStoreDefinition(config) {
1729
1929
  throw err;
1730
1930
  }
1731
1931
  finally {
1732
- loading.value = false;
1932
+ endLoading();
1733
1933
  }
1734
1934
  }
1735
1935
  async function updateReply(replyId, body) {
@@ -1962,16 +2162,23 @@ export function createDiscussionStoreDefinition(config) {
1962
2162
  spaceMembershipLoading,
1963
2163
  cleanupRealtimeChannels,
1964
2164
  threads,
2165
+ browseThreadList,
2166
+ currentBrowseMode,
1965
2167
  currentThread,
1966
2168
  replies,
2169
+ currentReplySort,
1967
2170
  loading,
1968
2171
  error,
1969
2172
  nextCursor,
2173
+ browseNextCursor,
1970
2174
  repliesNextCursor,
1971
2175
  spacesLoadingState,
1972
2176
  threadsLoadingState,
1973
2177
  repliesLoadingState,
1974
2178
  loadSpaces,
2179
+ createSpace,
2180
+ moveSpace,
2181
+ reorderSpaces,
1975
2182
  loadSpaceDetail,
1976
2183
  loadSpaceMembership,
1977
2184
  joinSpace,
@@ -1981,6 +2188,7 @@ export function createDiscussionStoreDefinition(config) {
1981
2188
  rootSpaces,
1982
2189
  leafSpaces,
1983
2190
  loadThreads,
2191
+ browseThreads,
1984
2192
  loadThread,
1985
2193
  loadReplies,
1986
2194
  createThread,