2026-09-14 21:14:23 +03:00
|
|
|
import { storeToRefs } from 'pinia'
|
2026-09-16 03:06:29 +03:00
|
|
|
import { computed, provide, ref, watch, nextTick } from 'vue'
|
2026-09-09 14:46:40 +03:00
|
|
|
|
2026-09-14 21:14:23 +03:00
|
|
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
2026-09-09 14:46:40 +03:00
|
|
|
import { useOAuthStore } from 'src/stores/oauth.js'
|
|
|
|
|
import { useStatusesStore } from 'src/stores/statuses.js'
|
2026-09-14 21:14:23 +03:00
|
|
|
import { useStreamingStore } from 'src/stores/streaming.js'
|
|
|
|
|
|
|
|
|
|
import { useMainStatus } from 'src/composables/useMainStatus.js'
|
2026-09-09 14:46:40 +03:00
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
fetchConversation as apiFetchConversation,
|
|
|
|
|
fetchStatus as apiFetchStatus,
|
|
|
|
|
} from 'src/api/public.js'
|
2026-09-15 01:11:30 +03:00
|
|
|
import { WSConnectionStatus } from 'src/api/websocket.js'
|
2026-09-09 14:46:40 +03:00
|
|
|
|
|
|
|
|
export function useConversation(statusId, expanded) {
|
2026-09-14 21:14:23 +03:00
|
|
|
const loadError = ref(null)
|
|
|
|
|
const { status: currentStatus, mainStatus } = useMainStatus(statusId)
|
2026-09-16 00:05:46 +03:00
|
|
|
const mainStatusId = computed(() => mainStatus.value?.id)
|
2026-09-14 21:14:23 +03:00
|
|
|
|
|
|
|
|
// # Config
|
|
|
|
|
const { mergedConfig } = storeToRefs(useMergedConfigStore())
|
|
|
|
|
const { mastoUserSocketStatus } = storeToRefs(useStreamingStore())
|
|
|
|
|
const streamingEnabled = computed(
|
|
|
|
|
() =>
|
|
|
|
|
mergedConfig.value.useStreamingApi &&
|
|
|
|
|
mastoUserSocketStatus === WSConnectionStatus.JOINED,
|
|
|
|
|
)
|
|
|
|
|
|
2026-09-16 00:05:46 +03:00
|
|
|
watch(statusId, (neu, old) => {
|
|
|
|
|
if (neu !== old) {
|
2026-09-15 01:07:56 +03:00
|
|
|
fetchConversation()
|
|
|
|
|
}
|
2026-09-14 21:14:23 +03:00
|
|
|
})
|
2026-09-15 00:59:21 +03:00
|
|
|
|
2026-09-09 14:46:40 +03:00
|
|
|
const sortById = (a, b) => {
|
|
|
|
|
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
|
|
|
|
|
const idB = b.type === 'retweet' ? b.retweeted_status.id : b.id
|
|
|
|
|
const seqA = Number(idA)
|
|
|
|
|
const seqB = Number(idB)
|
|
|
|
|
const isSeqA = !Number.isNaN(seqA)
|
|
|
|
|
const isSeqB = !Number.isNaN(seqB)
|
|
|
|
|
if (isSeqA && isSeqB) {
|
|
|
|
|
return seqA < seqB ? -1 : 1
|
|
|
|
|
} else if (isSeqA && !isSeqB) {
|
|
|
|
|
return -1
|
|
|
|
|
} else if (!isSeqA && isSeqB) {
|
|
|
|
|
return 1
|
|
|
|
|
} else {
|
|
|
|
|
return idA < idB ? -1 : 1
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-16 13:31:09 +03:00
|
|
|
const fullConversation = ref(new Set([mainStatus.value?.id].filter(Boolean)))
|
2026-09-16 03:06:29 +03:00
|
|
|
const fullyLoaded = ref(false)
|
2026-09-14 21:14:23 +03:00
|
|
|
const conversationId = computed(
|
2026-09-16 00:05:46 +03:00
|
|
|
() => mainStatus.value?.statusnet_conversation_id,
|
2026-09-14 21:14:23 +03:00
|
|
|
)
|
2026-09-16 03:06:29 +03:00
|
|
|
watch(conversationId, (neu, old) => {
|
|
|
|
|
if (neu !== old) fullyLoaded.value = false
|
|
|
|
|
})
|
2026-09-09 14:46:40 +03:00
|
|
|
const conversation = computed(() => {
|
|
|
|
|
if (!currentStatus.value) {
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!expanded.value) {
|
|
|
|
|
return [currentStatus.value]
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 03:06:29 +03:00
|
|
|
return [...fullConversation.value.keys()]
|
2026-09-09 14:46:40 +03:00
|
|
|
.map((k) => useStatusesStore().allStatuses.get(k))
|
|
|
|
|
.filter((status) => status.type != 'repeat') // Old backend behavior?
|
|
|
|
|
.toSorted(sortById)
|
|
|
|
|
})
|
2026-09-16 03:06:29 +03:00
|
|
|
|
2026-09-09 14:46:40 +03:00
|
|
|
const replies = computed(() =>
|
|
|
|
|
conversation.value.reduce(
|
|
|
|
|
(result, { id, in_reply_to_status_id: irid }, index) => {
|
|
|
|
|
if (irid) {
|
|
|
|
|
if (!result.has(irid)) {
|
|
|
|
|
result.set(irid, new Set())
|
|
|
|
|
}
|
|
|
|
|
result.get(irid).add({
|
|
|
|
|
name: `#${index}`,
|
|
|
|
|
id,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
},
|
|
|
|
|
new Map(),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
const getReplies = (id) => replies.value.get(id) ?? new Set()
|
|
|
|
|
provide('conversation', conversation)
|
|
|
|
|
provide('replies', replies)
|
|
|
|
|
|
|
|
|
|
const fetchConversation = async () => {
|
|
|
|
|
if (currentStatus.value) {
|
|
|
|
|
const {
|
|
|
|
|
data: { ancestors, descendants },
|
|
|
|
|
timestamp,
|
|
|
|
|
} = await apiFetchConversation({
|
|
|
|
|
id: statusId.value,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
useStatusesStore().addNewStatuses({ statuses: ancestors, timestamp })
|
|
|
|
|
useStatusesStore().addNewStatuses({
|
|
|
|
|
statuses: descendants,
|
|
|
|
|
timestamp,
|
|
|
|
|
})
|
2026-09-16 02:04:23 +03:00
|
|
|
|
|
|
|
|
fullConversation.value = new Set([
|
|
|
|
|
...ancestors,
|
2026-09-16 13:31:09 +03:00
|
|
|
mainStatus.value,
|
2026-09-16 02:04:23 +03:00
|
|
|
...descendants
|
|
|
|
|
].map(({ id }) => id))
|
|
|
|
|
|
2026-09-16 03:06:29 +03:00
|
|
|
await nextTick()
|
|
|
|
|
fullyLoaded.value = true
|
2026-09-09 14:46:40 +03:00
|
|
|
} else {
|
|
|
|
|
try {
|
|
|
|
|
loadError.value = null
|
|
|
|
|
|
|
|
|
|
const { data: status } = await apiFetchStatus({
|
|
|
|
|
id: statusId.value,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
useStatusesStore().addNewStatuses({ statuses: [status] })
|
2026-09-16 02:04:23 +03:00
|
|
|
fullConversation.value = new Set([
|
|
|
|
|
currentStatus.value,
|
|
|
|
|
].map(({ id }) => id))
|
2026-09-09 14:46:40 +03:00
|
|
|
fetchConversation()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(error)
|
|
|
|
|
loadError.value = error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 03:06:29 +03:00
|
|
|
// # Focus
|
|
|
|
|
const focused = ref(null)
|
|
|
|
|
const { mainStatus: focusedStatus } = useMainStatus(focused)
|
|
|
|
|
const setFocused = (id) => {
|
|
|
|
|
focused.value = id
|
|
|
|
|
}
|
|
|
|
|
watch(statusId, (val) => setFocused(val), { immediate: true })
|
|
|
|
|
|
|
|
|
|
const focusedId = computed(() => (expanded.value && fullyLoaded.value) ? focusedStatus.value?.id : null)
|
|
|
|
|
provide('focusedId', focusedId)
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
focusedStatus,
|
|
|
|
|
(newVal, oldVal) => {
|
|
|
|
|
if (!newVal) return
|
|
|
|
|
if (newVal?.id === oldVal?.id) return // prevents infinite loop
|
|
|
|
|
if (!streamingEnabled.value) {
|
|
|
|
|
useStatusesStore().fetchStatus(newVal.id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
useStatusesStore().fetchFavsAndRepeats(newVal.id)
|
|
|
|
|
useStatusesStore().fetchEmojiReactions(newVal.id)
|
|
|
|
|
},
|
|
|
|
|
{ immediate: true },
|
|
|
|
|
)
|
|
|
|
|
|
2026-09-09 14:46:40 +03:00
|
|
|
return {
|
2026-09-14 21:14:23 +03:00
|
|
|
focusedId,
|
2026-09-16 00:05:46 +03:00
|
|
|
conversationId,
|
2026-09-14 21:14:23 +03:00
|
|
|
setFocused,
|
2026-09-09 14:46:40 +03:00
|
|
|
currentStatus,
|
2026-09-10 19:10:01 +03:00
|
|
|
mainStatus,
|
2026-09-09 14:46:40 +03:00
|
|
|
conversation,
|
|
|
|
|
replies,
|
|
|
|
|
getReplies,
|
|
|
|
|
fetchConversation,
|
|
|
|
|
loadError,
|
|
|
|
|
}
|
|
|
|
|
}
|