@codingfactory/socialkit-vue 0.7.23 → 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.
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/services/circles.d.ts +7 -0
- package/dist/services/circles.d.ts.map +1 -1
- package/dist/services/circles.js +34 -5
- package/dist/services/circles.js.map +1 -1
- package/dist/stores/__tests__/discussion.spec.d.ts +2 -0
- package/dist/stores/__tests__/discussion.spec.d.ts.map +1 -0
- package/dist/stores/__tests__/discussion.spec.js +768 -0
- package/dist/stores/__tests__/discussion.spec.js.map +1 -0
- package/dist/stores/circles.d.ts +144 -0
- package/dist/stores/circles.d.ts.map +1 -1
- package/dist/stores/circles.js +7 -1
- package/dist/stores/circles.js.map +1 -1
- package/dist/stores/content.d.ts.map +1 -1
- package/dist/stores/content.js +6 -3
- package/dist/stores/content.js.map +1 -1
- package/dist/stores/discussion.d.ts +714 -15
- package/dist/stores/discussion.d.ts.map +1 -1
- package/dist/stores/discussion.js +272 -65
- package/dist/stores/discussion.js.map +1 -1
- package/dist/types/content.d.ts +3 -2
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/content.js +2 -1
- package/dist/types/content.js.map +1 -1
- package/dist/types/discussion.d.ts +38 -0
- package/dist/types/discussion.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +4 -0
- package/src/services/circles.ts +45 -5
- package/src/stores/__tests__/discussion.spec.ts +945 -0
- package/src/stores/circles.ts +7 -1
- package/src/stores/content.ts +6 -3
- package/src/stores/discussion.ts +333 -76
- package/src/types/content.ts +3 -2
- package/src/types/discussion.ts +43 -0
package/src/stores/discussion.ts
CHANGED
|
@@ -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):
|
|
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
|
-
|
|
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
|
-
|
|
736
|
+
beginLoading()
|
|
681
737
|
error.value = null
|
|
682
738
|
|
|
683
739
|
try {
|
|
684
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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,29 +1115,29 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
932
1115
|
latestThreadId.value = threadId
|
|
933
1116
|
|
|
934
1117
|
cleanupRealtimeChannels()
|
|
935
|
-
|
|
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
|
-
|
|
1126
|
+
endLoading()
|
|
941
1127
|
return null
|
|
942
1128
|
}
|
|
943
1129
|
|
|
944
1130
|
// Accept both UUID and slug identifiers — the backend resolves by ID or slug
|
|
945
1131
|
if (!/^[\w-]+$/u.test(threadId)) {
|
|
946
1132
|
error.value = 'Invalid thread identifier format'
|
|
947
|
-
|
|
1133
|
+
endLoading()
|
|
948
1134
|
return null
|
|
949
1135
|
}
|
|
950
1136
|
|
|
951
|
-
let isStale = false
|
|
952
|
-
|
|
953
1137
|
try {
|
|
954
1138
|
const response = await client.get<unknown>(`/v1/discussion/threads/${threadId}`)
|
|
955
1139
|
|
|
956
1140
|
if (latestThreadId.value !== threadId) {
|
|
957
|
-
isStale = true
|
|
958
1141
|
return null
|
|
959
1142
|
}
|
|
960
1143
|
|
|
@@ -1011,7 +1194,6 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1011
1194
|
return currentThread.value
|
|
1012
1195
|
} catch (err) {
|
|
1013
1196
|
if (latestThreadId.value !== threadId) {
|
|
1014
|
-
isStale = true
|
|
1015
1197
|
return null
|
|
1016
1198
|
}
|
|
1017
1199
|
|
|
@@ -1037,9 +1219,7 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1037
1219
|
|
|
1038
1220
|
throw err
|
|
1039
1221
|
} finally {
|
|
1040
|
-
|
|
1041
|
-
loading.value = false
|
|
1042
|
-
}
|
|
1222
|
+
endLoading()
|
|
1043
1223
|
}
|
|
1044
1224
|
}
|
|
1045
1225
|
|
|
@@ -1052,11 +1232,9 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1052
1232
|
latestThreadId.value = threadId
|
|
1053
1233
|
}
|
|
1054
1234
|
|
|
1055
|
-
|
|
1235
|
+
beginLoading()
|
|
1056
1236
|
error.value = null
|
|
1057
1237
|
|
|
1058
|
-
let isStale = false
|
|
1059
|
-
|
|
1060
1238
|
try {
|
|
1061
1239
|
const queryParts: string[] = []
|
|
1062
1240
|
if (cursor) {
|
|
@@ -1070,7 +1248,6 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1070
1248
|
const response = await client.get<unknown>(`/v1/discussion/threads/${threadId}/replies${queryString}`)
|
|
1071
1249
|
|
|
1072
1250
|
if (latestThreadId.value !== threadId) {
|
|
1073
|
-
isStale = true
|
|
1074
1251
|
return undefined
|
|
1075
1252
|
}
|
|
1076
1253
|
|
|
@@ -1139,7 +1316,6 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1139
1316
|
return response.data
|
|
1140
1317
|
} catch (err) {
|
|
1141
1318
|
if (latestThreadId.value !== threadId) {
|
|
1142
|
-
isStale = true
|
|
1143
1319
|
return undefined
|
|
1144
1320
|
}
|
|
1145
1321
|
|
|
@@ -1147,9 +1323,7 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1147
1323
|
error.value = getErrorMessage(err, 'Failed to load replies')
|
|
1148
1324
|
throw err
|
|
1149
1325
|
} finally {
|
|
1150
|
-
|
|
1151
|
-
loading.value = false
|
|
1152
|
-
}
|
|
1326
|
+
endLoading()
|
|
1153
1327
|
}
|
|
1154
1328
|
}
|
|
1155
1329
|
|
|
@@ -1168,10 +1342,19 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1168
1342
|
if (existingReplyIndex !== -1) {
|
|
1169
1343
|
const existingReply = replies.value[existingReplyIndex]
|
|
1170
1344
|
if (existingReply) {
|
|
1171
|
-
|
|
1345
|
+
const merged = {
|
|
1172
1346
|
...existingReply,
|
|
1173
1347
|
...reply,
|
|
1174
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
|
|
1175
1358
|
}
|
|
1176
1359
|
|
|
1177
1360
|
return false
|
|
@@ -1222,7 +1405,7 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1222
1405
|
input: CreateThreadInput
|
|
1223
1406
|
): Promise<DiscussionCreateThreadResponse> {
|
|
1224
1407
|
cleanupRealtimeChannels()
|
|
1225
|
-
|
|
1408
|
+
beginLoading()
|
|
1226
1409
|
error.value = null
|
|
1227
1410
|
|
|
1228
1411
|
try {
|
|
@@ -1253,12 +1436,12 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1253
1436
|
error.value = getErrorMessage(err, 'Failed to create thread')
|
|
1254
1437
|
throw err
|
|
1255
1438
|
} finally {
|
|
1256
|
-
|
|
1439
|
+
endLoading()
|
|
1257
1440
|
}
|
|
1258
1441
|
}
|
|
1259
1442
|
|
|
1260
1443
|
async function loadTagCategories(query = ''): Promise<DiscussionTag[]> {
|
|
1261
|
-
|
|
1444
|
+
beginLoading()
|
|
1262
1445
|
error.value = null
|
|
1263
1446
|
|
|
1264
1447
|
try {
|
|
@@ -1293,7 +1476,7 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1293
1476
|
error.value = getErrorMessage(err, 'Failed to load tag suggestions')
|
|
1294
1477
|
throw err
|
|
1295
1478
|
} finally {
|
|
1296
|
-
|
|
1479
|
+
endLoading()
|
|
1297
1480
|
}
|
|
1298
1481
|
}
|
|
1299
1482
|
|
|
@@ -1597,13 +1780,19 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1597
1780
|
return
|
|
1598
1781
|
}
|
|
1599
1782
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
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)
|
|
1607
1796
|
}
|
|
1608
1797
|
|
|
1609
1798
|
function handleRealtimeReplyReaction(payload: ReplyReactionRealtimePayload | null | undefined): void {
|
|
@@ -1741,8 +1930,14 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1741
1930
|
}
|
|
1742
1931
|
}
|
|
1743
1932
|
|
|
1744
|
-
async function searchThreads(
|
|
1745
|
-
|
|
1933
|
+
async function searchThreads(
|
|
1934
|
+
query: string,
|
|
1935
|
+
spaceSlug?: string,
|
|
1936
|
+
options?: {
|
|
1937
|
+
signal?: AbortSignal
|
|
1938
|
+
},
|
|
1939
|
+
): Promise<unknown> {
|
|
1940
|
+
beginLoading()
|
|
1746
1941
|
error.value = null
|
|
1747
1942
|
|
|
1748
1943
|
try {
|
|
@@ -1751,33 +1946,49 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1751
1946
|
params.append('space', spaceSlug)
|
|
1752
1947
|
}
|
|
1753
1948
|
|
|
1754
|
-
const
|
|
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)
|
|
1755
1953
|
return response.data
|
|
1756
1954
|
} catch (err) {
|
|
1955
|
+
if (options?.signal?.aborted) {
|
|
1956
|
+
return undefined
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1757
1959
|
logger.error('Failed to search threads:', err)
|
|
1758
1960
|
error.value = getErrorMessage(err, 'Failed to search')
|
|
1759
1961
|
throw err
|
|
1760
1962
|
} finally {
|
|
1761
|
-
|
|
1963
|
+
endLoading()
|
|
1762
1964
|
}
|
|
1763
1965
|
}
|
|
1764
1966
|
|
|
1765
|
-
async function searchThreadsInSpace(
|
|
1967
|
+
async function searchThreadsInSpace(
|
|
1968
|
+
query: string,
|
|
1969
|
+
spaceId: string,
|
|
1970
|
+
options?: {
|
|
1971
|
+
signal?: AbortSignal
|
|
1972
|
+
},
|
|
1973
|
+
): Promise<Thread[]> {
|
|
1766
1974
|
const normalizedQuery = query.trim()
|
|
1767
1975
|
if (normalizedQuery.length < 2) {
|
|
1768
1976
|
return []
|
|
1769
1977
|
}
|
|
1770
1978
|
|
|
1771
|
-
|
|
1979
|
+
beginLoading()
|
|
1772
1980
|
error.value = null
|
|
1773
1981
|
|
|
1774
1982
|
try {
|
|
1983
|
+
const requestConfig = options?.signal
|
|
1984
|
+
? { signal: options.signal }
|
|
1985
|
+
: undefined
|
|
1775
1986
|
const response = await client.post<ThreadSearchResponseBody>('/v1/discussion/search', {
|
|
1776
1987
|
query: normalizedQuery,
|
|
1777
1988
|
space_id: spaceId,
|
|
1778
1989
|
sort_by: 'relevance',
|
|
1779
1990
|
limit: 50,
|
|
1780
|
-
})
|
|
1991
|
+
}, requestConfig)
|
|
1781
1992
|
|
|
1782
1993
|
const rows = extractThreadSearchRows(response.data)
|
|
1783
1994
|
if (rows.length > 0) {
|
|
@@ -1786,41 +1997,57 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1786
1997
|
|
|
1787
1998
|
return filterLoadedThreadsByQuery(normalizedQuery, spaceId)
|
|
1788
1999
|
} catch (err) {
|
|
2000
|
+
if (options?.signal?.aborted) {
|
|
2001
|
+
return []
|
|
2002
|
+
}
|
|
2003
|
+
|
|
1789
2004
|
logger.error('Failed to search threads in space:', err)
|
|
1790
2005
|
error.value = isAxiosError(err)
|
|
1791
2006
|
? err.response?.data?.message ?? 'Failed to search threads'
|
|
1792
2007
|
: 'Failed to search threads'
|
|
1793
2008
|
throw err
|
|
1794
2009
|
} finally {
|
|
1795
|
-
|
|
2010
|
+
endLoading()
|
|
1796
2011
|
}
|
|
1797
2012
|
}
|
|
1798
2013
|
|
|
1799
|
-
async function searchThreadsGlobally(
|
|
2014
|
+
async function searchThreadsGlobally(
|
|
2015
|
+
query: string,
|
|
2016
|
+
options?: {
|
|
2017
|
+
signal?: AbortSignal
|
|
2018
|
+
},
|
|
2019
|
+
): Promise<Thread[]> {
|
|
1800
2020
|
const normalizedQuery = query.trim()
|
|
1801
2021
|
if (normalizedQuery.length < 2) {
|
|
1802
2022
|
return []
|
|
1803
2023
|
}
|
|
1804
2024
|
|
|
1805
|
-
|
|
2025
|
+
beginLoading()
|
|
1806
2026
|
error.value = null
|
|
1807
2027
|
|
|
1808
2028
|
try {
|
|
2029
|
+
const requestConfig = options?.signal
|
|
2030
|
+
? { signal: options.signal }
|
|
2031
|
+
: undefined
|
|
1809
2032
|
const response = await client.post<ThreadSearchResponseBody>('/v1/discussion/search', {
|
|
1810
2033
|
query: normalizedQuery,
|
|
1811
2034
|
sort_by: 'relevance',
|
|
1812
2035
|
limit: 50,
|
|
1813
|
-
})
|
|
2036
|
+
}, requestConfig)
|
|
1814
2037
|
|
|
1815
2038
|
return extractThreadSearchRows(response.data).map((row) => mapSearchRowToThread(row))
|
|
1816
2039
|
} catch (err) {
|
|
2040
|
+
if (options?.signal?.aborted) {
|
|
2041
|
+
return []
|
|
2042
|
+
}
|
|
2043
|
+
|
|
1817
2044
|
logger.error('Failed to search threads globally:', err)
|
|
1818
2045
|
error.value = isAxiosError(err)
|
|
1819
2046
|
? err.response?.data?.message ?? 'Failed to search threads'
|
|
1820
2047
|
: 'Failed to search threads'
|
|
1821
2048
|
throw err
|
|
1822
2049
|
} finally {
|
|
1823
|
-
|
|
2050
|
+
endLoading()
|
|
1824
2051
|
}
|
|
1825
2052
|
}
|
|
1826
2053
|
|
|
@@ -1852,6 +2079,15 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1852
2079
|
thread.slug = row.slug
|
|
1853
2080
|
}
|
|
1854
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
|
+
|
|
1855
2091
|
return thread
|
|
1856
2092
|
}
|
|
1857
2093
|
|
|
@@ -1897,7 +2133,7 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1897
2133
|
}
|
|
1898
2134
|
|
|
1899
2135
|
async function setThreadPinned(threadId: string, pinned: boolean): Promise<void> {
|
|
1900
|
-
|
|
2136
|
+
beginLoading()
|
|
1901
2137
|
error.value = null
|
|
1902
2138
|
|
|
1903
2139
|
try {
|
|
@@ -1918,12 +2154,12 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1918
2154
|
error.value = getErrorMessage(err, 'Failed to update pinned status')
|
|
1919
2155
|
throw err
|
|
1920
2156
|
} finally {
|
|
1921
|
-
|
|
2157
|
+
endLoading()
|
|
1922
2158
|
}
|
|
1923
2159
|
}
|
|
1924
2160
|
|
|
1925
2161
|
async function setThreadLocked(threadId: string, locked: boolean): Promise<void> {
|
|
1926
|
-
|
|
2162
|
+
beginLoading()
|
|
1927
2163
|
error.value = null
|
|
1928
2164
|
|
|
1929
2165
|
try {
|
|
@@ -1943,12 +2179,12 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1943
2179
|
error.value = getErrorMessage(err, 'Failed to update locked status')
|
|
1944
2180
|
throw err
|
|
1945
2181
|
} finally {
|
|
1946
|
-
|
|
2182
|
+
endLoading()
|
|
1947
2183
|
}
|
|
1948
2184
|
}
|
|
1949
2185
|
|
|
1950
2186
|
async function moveThread(threadId: string, toSpaceSlug: string): Promise<void> {
|
|
1951
|
-
|
|
2187
|
+
beginLoading()
|
|
1952
2188
|
error.value = null
|
|
1953
2189
|
|
|
1954
2190
|
try {
|
|
@@ -1959,12 +2195,22 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1959
2195
|
const responseData = toRecord(response.data)
|
|
1960
2196
|
const movedThread = (responseData?.data ?? null) as Thread | null
|
|
1961
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
|
|
1962
2205
|
|
|
1963
2206
|
if (currentThread.value?.id === threadId && currentThread.value) {
|
|
1964
2207
|
currentThread.value = {
|
|
1965
2208
|
...currentThread.value,
|
|
1966
2209
|
...(movedThread ?? {}),
|
|
1967
|
-
...(
|
|
2210
|
+
...(destinationSpaceSummary ? {
|
|
2211
|
+
space_id: destinationSpaceSummary.id,
|
|
2212
|
+
space: destinationSpaceSummary,
|
|
2213
|
+
} : {}),
|
|
1968
2214
|
}
|
|
1969
2215
|
}
|
|
1970
2216
|
|
|
@@ -1982,7 +2228,10 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1982
2228
|
threads.value[threadIndex] = {
|
|
1983
2229
|
...existingThread,
|
|
1984
2230
|
...(movedThread ?? {}),
|
|
1985
|
-
...(
|
|
2231
|
+
...(destinationSpaceSummary ? {
|
|
2232
|
+
space_id: destinationSpaceSummary.id,
|
|
2233
|
+
space: destinationSpaceSummary,
|
|
2234
|
+
} : {}),
|
|
1986
2235
|
}
|
|
1987
2236
|
}
|
|
1988
2237
|
}
|
|
@@ -1993,12 +2242,12 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
1993
2242
|
error.value = getErrorMessage(err, 'Failed to move thread')
|
|
1994
2243
|
throw err
|
|
1995
2244
|
} finally {
|
|
1996
|
-
|
|
2245
|
+
endLoading()
|
|
1997
2246
|
}
|
|
1998
2247
|
}
|
|
1999
2248
|
|
|
2000
2249
|
async function updateThread(threadId: string, updates: UpdateThreadInput): Promise<unknown> {
|
|
2001
|
-
|
|
2250
|
+
beginLoading()
|
|
2002
2251
|
error.value = null
|
|
2003
2252
|
|
|
2004
2253
|
try {
|
|
@@ -2031,12 +2280,12 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
2031
2280
|
error.value = getErrorMessage(err, 'Failed to update thread')
|
|
2032
2281
|
throw err
|
|
2033
2282
|
} finally {
|
|
2034
|
-
|
|
2283
|
+
endLoading()
|
|
2035
2284
|
}
|
|
2036
2285
|
}
|
|
2037
2286
|
|
|
2038
2287
|
async function deleteThread(threadId: string): Promise<unknown> {
|
|
2039
|
-
|
|
2288
|
+
beginLoading()
|
|
2040
2289
|
error.value = null
|
|
2041
2290
|
|
|
2042
2291
|
try {
|
|
@@ -2054,12 +2303,12 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
2054
2303
|
error.value = getErrorMessage(err, 'Failed to delete thread')
|
|
2055
2304
|
throw err
|
|
2056
2305
|
} finally {
|
|
2057
|
-
|
|
2306
|
+
endLoading()
|
|
2058
2307
|
}
|
|
2059
2308
|
}
|
|
2060
2309
|
|
|
2061
2310
|
async function restoreThread(threadId: string): Promise<Thread> {
|
|
2062
|
-
|
|
2311
|
+
beginLoading()
|
|
2063
2312
|
error.value = null
|
|
2064
2313
|
|
|
2065
2314
|
try {
|
|
@@ -2102,12 +2351,12 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
2102
2351
|
error.value = getErrorMessage(err, 'Failed to restore thread')
|
|
2103
2352
|
throw err
|
|
2104
2353
|
} finally {
|
|
2105
|
-
|
|
2354
|
+
endLoading()
|
|
2106
2355
|
}
|
|
2107
2356
|
}
|
|
2108
2357
|
|
|
2109
2358
|
async function deleteReply(replyId: string): Promise<void> {
|
|
2110
|
-
|
|
2359
|
+
beginLoading()
|
|
2111
2360
|
error.value = null
|
|
2112
2361
|
|
|
2113
2362
|
const replyIndex = replies.value.findIndex((reply) => reply.id === replyId)
|
|
@@ -2171,7 +2420,7 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
2171
2420
|
error.value = getErrorMessage(err, 'Failed to delete reply')
|
|
2172
2421
|
throw err
|
|
2173
2422
|
} finally {
|
|
2174
|
-
|
|
2423
|
+
endLoading()
|
|
2175
2424
|
}
|
|
2176
2425
|
}
|
|
2177
2426
|
|
|
@@ -2432,16 +2681,23 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
2432
2681
|
spaceMembershipLoading,
|
|
2433
2682
|
cleanupRealtimeChannels,
|
|
2434
2683
|
threads,
|
|
2684
|
+
browseThreadList,
|
|
2685
|
+
currentBrowseMode,
|
|
2435
2686
|
currentThread,
|
|
2436
2687
|
replies,
|
|
2688
|
+
currentReplySort,
|
|
2437
2689
|
loading,
|
|
2438
2690
|
error,
|
|
2439
2691
|
nextCursor,
|
|
2692
|
+
browseNextCursor,
|
|
2440
2693
|
repliesNextCursor,
|
|
2441
2694
|
spacesLoadingState,
|
|
2442
2695
|
threadsLoadingState,
|
|
2443
2696
|
repliesLoadingState,
|
|
2444
2697
|
loadSpaces,
|
|
2698
|
+
createSpace,
|
|
2699
|
+
moveSpace,
|
|
2700
|
+
reorderSpaces,
|
|
2445
2701
|
loadSpaceDetail,
|
|
2446
2702
|
loadSpaceMembership,
|
|
2447
2703
|
joinSpace,
|
|
@@ -2451,6 +2707,7 @@ export function createDiscussionStoreDefinition(config: DiscussionStoreConfig) {
|
|
|
2451
2707
|
rootSpaces,
|
|
2452
2708
|
leafSpaces,
|
|
2453
2709
|
loadThreads,
|
|
2710
|
+
browseThreads,
|
|
2454
2711
|
loadThread,
|
|
2455
2712
|
loadReplies,
|
|
2456
2713
|
createThread,
|