conversation view works

This commit is contained in:
Henry Jameson 2026-08-13 19:49:15 +03:00
commit 4cbc3d3aa1
3 changed files with 118 additions and 63 deletions

View file

@ -1,7 +1,6 @@
import { get, maxBy, minBy, sortBy, throttle } from 'lodash' import { get, maxBy, minBy, sortBy, throttle } from 'lodash'
import { mapState as mapPiniaState } from 'pinia' import { mapState } from 'pinia'
import { nextTick } from 'vue' import { nextTick } from 'vue'
import { mapState } from 'vuex'
import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue' import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue'
import ChatTitle from 'src/components/chat_title/chat_title.vue' import ChatTitle from 'src/components/chat_title/chat_title.vue'
@ -20,6 +19,7 @@ import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useStatusesStore } from 'src/stores/statuses.js' import { useStatusesStore } from 'src/stores/statuses.js'
import { useStreamingStore } from 'src/stores/streaming.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
import { import {
@ -87,6 +87,8 @@ const Chat = {
// Internal network stuff // Internal network stuff
fetcher: null, fetcher: null,
socket: null,
streaming: false,
errorLoadingChat: false, errorLoadingChat: false,
messageRetriers: {}, messageRetriers: {},
idempotencyKeyIndex: {}, idempotencyKeyIndex: {},
@ -94,7 +96,8 @@ const Chat = {
}, },
created() { created() {
if (this.testMode) return if (this.testMode) return
this.startFetching() this.activate()
this.attachSocket()
}, },
mounted() { mounted() {
window.addEventListener('resize', this.handleResize) window.addEventListener('resize', this.handleResize)
@ -120,6 +123,9 @@ const Chat = {
this.handleVisibilityChange, this.handleVisibilityChange,
false, false,
) )
if (this.testMode) return
this.deactivate()
}, },
computed: { computed: {
conversationId() { conversationId() {
@ -159,17 +165,14 @@ const Chat = {
if (this.isConversation) return false // Unsupported if (this.isConversation) return false // Unsupported
return ( return (
this.mergedConfig.useStreamingApi && this.mergedConfig.useStreamingApi &&
this.mastoUserSocketStatus === WSConnectionStatus.JOINED useStreamingStore().state === WSConnectionStatus.JOINED
) )
}, },
...mapPiniaState(useInterfaceStore, { ...mapState(useInterfaceStore, {
mobileLayout: (store) => store.layoutType === 'mobile', mobileLayout: (store) => store.layoutType === 'mobile',
}), }),
...mapPiniaState(useMergedConfigStore, ['mergedConfig']), ...mapState(useMergedConfigStore, ['mergedConfig']),
...mapPiniaState(useUsersStore, ['currentUser']), ...mapState(useUsersStore, ['currentUser']),
...mapState({
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
}),
}, },
watch: { watch: {
messages(old, neu) { messages(old, neu) {
@ -228,16 +231,85 @@ const Chat = {
return return
} }
this.clear() this.deactivate()
this.startFetching() this.activate()
},
mastoUserSocketStatus(newValue) {
if (newValue === WSConnectionStatus.JOINED) {
this.fetchChat({ isFirstFetch: true })
}
}, },
}, },
methods: { methods: {
async activate() {
if (!this.isConversation) {
try {
const result = await getOrCreateChat({
accountId: this.chatUserId,
credentials: useOAuthStore().token,
})
useUsersStore().addNewUsers(result)
const { data } = result
data.account = useUsersStore().findUser(data.account.id)
this.chat = data
} catch (e) {
console.error('Error creating or getting a chat', e)
this.errorLoadingChat = true
}
}
if (this.isConversation || this.chat) {
this.$nextTick(() => {
this.scrollDown({ forceRead: true })
})
this.startFetching('Chat activated', true)
}
},
deactivate() {
this.clear()
if (!this.streaming) {
this.stopFetching()
}
},
attachSocket() {
const et = new EventTarget()
const socket = { et }
et.addEventListener('update', this.onStreamMessage)
et.addEventListener('open', this.onStreamConnect)
et.addEventListener('close', this.onStreamDisconnect)
useStreamingStore().addSubscriber(socket)
this.socket = socket
},
detachSocket() {
const { et } = this.socket
et.removeEventListener('update', this.onStreamMessage)
et.removeEventListener('open', this.onStreamConnect)
et.removeEventListener('close', this.onStreamDisconnect)
useStreamingStore().removeSubscriber(this.socket)
},
// Poll & Push
onStreamConnect() {
this.streaming = true
this.stopFetching('Socket connected')
},
onStreamDisconnect(closeEvent) {
this.streaming = false
this.startFetching('Socket disconnected')
},
startFetching(reason, isFirstFetch) {
console.debug('[Chat View] Started fetching', 'Reason:', reason)
this.fetcher = promiseInterval(
() => this.fetchChat({ fetchLatest: true }),
5000,
)
this.fetchChat({ isFirstFetch })
},
stopFetching(reason) {
console.debug('[Chat View] Stopped fetching', 'Reason:', reason)
this.fetcher.stop()
this.fetcher = null
},
// Actions // Actions
async readChat() { async readChat() {
if (this.conversationId) return // Unsupported if (this.conversationId) return // Unsupported
@ -261,18 +333,8 @@ const Chat = {
this.lastReadMessageId = this.maxId this.lastReadMessageId = this.maxId
this.newMessageCount = 0 this.newMessageCount = 0
}, },
scrollDown(options = {}) {
const { behavior = 'auto', forceRead = false } = options // Clears
this.$nextTick(() => {
window.scrollTo({
top: document.documentElement.scrollHeight,
behavior,
})
})
if (forceRead) {
this.readChat()
}
},
cullOlder() { cullOlder() {
const maxIndex = this.messages.length const maxIndex = this.messages.length
const minIndex = maxIndex - 50 const minIndex = maxIndex - 50
@ -368,36 +430,12 @@ const Chat = {
}) })
} }
}, },
async startFetching() { onStreamMessage({ data }) {
if (!this.isConversation) { const messages = data.filter(
try { ({ statusnet_conversation_id }) =>
const result = await getOrCreateChat({ statusnet_conversation_id === this.conversationId,
accountId: this.chatUserId,
credentials: useOAuthStore().token,
})
useUsersStore().addNewUsers(result)
const { data } = result
data.account = useUsersStore().findUser(data.account.id)
this.chat = data
} catch (e) {
console.error('Error creating or getting a chat', e)
this.errorLoadingChat = true
}
}
if (this.isConversation || this.chat) {
this.$nextTick(() => {
this.scrollDown({ forceRead: true })
})
this.doStartFetching()
}
},
doStartFetching() {
this.fetcher = promiseInterval(
() => this.fetchChat({ fetchLatest: true }),
5000,
) )
this.fetchChat({ isFirstFetch: true }) this.addMessages({ messages })
}, },
addMessages({ messages: newMessages }) { addMessages({ messages: newMessages }) {
for (let i = 0; i < newMessages.length; i++) { for (let i = 0; i < newMessages.length; i++) {
@ -441,9 +479,6 @@ const Chat = {
} }
} }
}, },
goBack() {
this.$router.back()
},
// Optimistic posting (chats only) // Optimistic posting (chats only)
async sendMessage({ status, media, idempotencyKey }) { async sendMessage({ status, media, idempotencyKey }) {
@ -621,6 +656,23 @@ const Chat = {
}) })
}, },
// Misc
scrollDown(options = {}) {
const { behavior = 'auto', forceRead = false } = options
this.$nextTick(() => {
window.scrollTo({
top: document.documentElement.scrollHeight,
behavior,
})
})
if (forceRead) {
this.readChat()
}
},
goBack() {
this.$router.back()
},
// Ugly // Ugly
// TODO move to ChatMessage // TODO move to ChatMessage
async deleteChatMessage({ chatId, messageId }) { async deleteChatMessage({ chatId, messageId }) {

View file

@ -451,7 +451,7 @@ const conversation = {
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}) })
.then(({ data: status }) => { .then(({ data: status }) => {
this.$store.dispatch('addNewStatuses', { statuses: [status] }) useStatusesStore().addNewStatuses({ statuses: [status] })
this.fetchConversation() this.fetchConversation()
}) })
.catch((error) => { .catch((error) => {

View file

@ -86,7 +86,9 @@ export const useStreamingStore = defineStore('streaming', {
this.subscribers.add(subscriber) this.subscribers.add(subscriber)
if (this.state === WSConnectionStatus.JOINED) { if (this.state === WSConnectionStatus.JOINED) {
this.socket.subscribe(...this.getSubArgs(stream)) if (stream) {
this.socket.subscribe(...this.getSubArgs(stream))
}
et.dispatchEvent(new StreamStateEvent('open')) et.dispatchEvent(new StreamStateEvent('open'))
} }
}, },
@ -171,6 +173,7 @@ export const useStreamingStore = defineStore('streaming', {
case 'delete': case 'delete':
return [data.id] return [data.id]
default: default:
console.log('UNKNOWN', eventName, eventStream, data)
return data return data
} }
})() })()