@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.
- 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 +274 -66
- 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 +335 -77
- 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,28 +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
|
|
1131
|
+
if (!/^[\w-]+$/u.test(threadId)) {
|
|
945
1132
|
error.value = 'Invalid thread identifier format'
|
|
946
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1439
|
+
endLoading()
|
|
1256
1440
|
}
|
|
1257
1441
|
}
|
|
1258
1442
|
|
|
1259
1443
|
async function loadTagCategories(query = ''): Promise<DiscussionTag[]> {
|
|
1260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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(
|
|
1744
|
-
|
|
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
|
|
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
|
-
|
|
1963
|
+
endLoading()
|
|
1761
1964
|
}
|
|
1762
1965
|
}
|
|
1763
1966
|
|
|
1764
|
-
async function searchThreadsInSpace(
|
|
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
|
-
|
|
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
|
-
|
|
2010
|
+
endLoading()
|
|
1795
2011
|
}
|
|
1796
2012
|
}
|
|
1797
2013
|
|
|
1798
|
-
async function searchThreadsGlobally(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2157
|
+
endLoading()
|
|
1921
2158
|
}
|
|
1922
2159
|
}
|
|
1923
2160
|
|
|
1924
2161
|
async function setThreadLocked(threadId: string, locked: boolean): Promise<void> {
|
|
1925
|
-
|
|
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
|
-
|
|
2182
|
+
endLoading()
|
|
1946
2183
|
}
|
|
1947
2184
|
}
|
|
1948
2185
|
|
|
1949
2186
|
async function moveThread(threadId: string, toSpaceSlug: string): Promise<void> {
|
|
1950
|
-
|
|
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
|
-
...(
|
|
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
|
-
...(
|
|
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
|
-
|
|
2245
|
+
endLoading()
|
|
1996
2246
|
}
|
|
1997
2247
|
}
|
|
1998
2248
|
|
|
1999
2249
|
async function updateThread(threadId: string, updates: UpdateThreadInput): Promise<unknown> {
|
|
2000
|
-
|
|
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
|
-
|
|
2283
|
+
endLoading()
|
|
2034
2284
|
}
|
|
2035
2285
|
}
|
|
2036
2286
|
|
|
2037
2287
|
async function deleteThread(threadId: string): Promise<unknown> {
|
|
2038
|
-
|
|
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
|
-
|
|
2306
|
+
endLoading()
|
|
2057
2307
|
}
|
|
2058
2308
|
}
|
|
2059
2309
|
|
|
2060
2310
|
async function restoreThread(threadId: string): Promise<Thread> {
|
|
2061
|
-
|
|
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
|
-
|
|
2354
|
+
endLoading()
|
|
2105
2355
|
}
|
|
2106
2356
|
}
|
|
2107
2357
|
|
|
2108
2358
|
async function deleteReply(replyId: string): Promise<void> {
|
|
2109
|
-
|
|
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
|
-
|
|
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,
|