remove chat mode from thread display settings, instead use dedicated page for chat convos
This commit is contained in:
parent
7b4fef365e
commit
c6ac897802
13 changed files with 303 additions and 249 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { maxBy, minBy, sortBy, throttle } from 'lodash'
|
||||
import { get, maxBy, minBy, sortBy, throttle } from 'lodash'
|
||||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import { nextTick } from 'vue'
|
||||
import { mapState } from 'vuex'
|
||||
|
|
@ -28,6 +28,7 @@ import {
|
|||
sendChatMessage,
|
||||
} from 'src/api/chats.js'
|
||||
import { WSConnectionStatus } from 'src/api/websocket.js'
|
||||
import { fetchConversation, fetchStatus } from 'src/api/public.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
|
||||
|
|
@ -52,6 +53,7 @@ const Chat = {
|
|||
PostStatusForm,
|
||||
},
|
||||
props: {
|
||||
statusId: String,
|
||||
testMode: Boolean,
|
||||
},
|
||||
data() {
|
||||
|
|
@ -65,6 +67,9 @@ const Chat = {
|
|||
minId: undefined,
|
||||
maxId: undefined,
|
||||
|
||||
// Conversation stuff
|
||||
explicitReplyStatus: null,
|
||||
|
||||
// Unread stuff
|
||||
newMessageCount: 0,
|
||||
lastReadMessageId: null,
|
||||
|
|
@ -108,6 +113,17 @@ const Chat = {
|
|||
)
|
||||
},
|
||||
computed: {
|
||||
conversationId() {
|
||||
const status = this.$store.state.statuses.allStatusesObject[this.statusId]
|
||||
return get(
|
||||
status,
|
||||
'retweeted_status.statusnet_conversation_id',
|
||||
get(status, 'statusnet_conversation_id'),
|
||||
)
|
||||
},
|
||||
isConversation() {
|
||||
return this.conversationId !== null
|
||||
},
|
||||
recipient() {
|
||||
return this.chat?.account
|
||||
},
|
||||
|
|
@ -123,7 +139,18 @@ const Chat = {
|
|||
return ''
|
||||
}
|
||||
},
|
||||
|
||||
// Conversation stuff
|
||||
lastStatus() {
|
||||
return this.messages[this.messages.length - 1]
|
||||
},
|
||||
replyStatus() {
|
||||
return this.explicitReplyStatus ?? this.lastStatus
|
||||
},
|
||||
|
||||
// Global Stuff
|
||||
streamingEnabled() {
|
||||
if (this.isConversation) return false // Unsupported
|
||||
return (
|
||||
this.mergedConfig.useStreamingApi &&
|
||||
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||
|
|
@ -142,13 +169,17 @@ const Chat = {
|
|||
messages() {
|
||||
// We don't want to scroll to the bottom on a new message when the user is viewing older messages.
|
||||
// Therefore we need to know whether the scroll position was at the bottom before the DOM update.
|
||||
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
|
||||
const bottomedOutBeforeUpdate = isBottomedOut(BOTTOMED_OUT_OFFSET)
|
||||
this.$nextTick(() => {
|
||||
if (bottomedOutBeforeUpdate) {
|
||||
this.scrollDown()
|
||||
}
|
||||
})
|
||||
},
|
||||
async replyStatus(newVal) {
|
||||
await nextTick() // wait for changes to propagate to postStatusForm
|
||||
this.$refs.postStatusForm.update()
|
||||
},
|
||||
$route: function () {
|
||||
this.startFetching()
|
||||
},
|
||||
|
|
@ -159,53 +190,9 @@ const Chat = {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
onFilesDropped() {
|
||||
this.$nextTick(() => {
|
||||
this.handleResize()
|
||||
})
|
||||
},
|
||||
handleVisibilityChange() {
|
||||
this.$nextTick(() => {
|
||||
if (!document.hidden && this.bottomedOut(BOTTOMED_OUT_OFFSET)) {
|
||||
this.scrollDown({ forceRead: true })
|
||||
}
|
||||
})
|
||||
},
|
||||
// "Sticks" scroll to bottom instead of top, helps with OSK resizing the viewport
|
||||
handleResize(opts = {}) {
|
||||
const { delayed = false } = opts
|
||||
|
||||
if (delayed) {
|
||||
setTimeout(() => {
|
||||
this.handleResize({ ...opts, delayed: false })
|
||||
}, SAFE_RESIZE_TIME_OFFSET)
|
||||
return
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
const { offsetHeight = undefined } = getScrollPosition()
|
||||
const diff = offsetHeight - this.lastScrollPosition.offsetHeight
|
||||
if (diff !== 0 && !this.bottomedOut()) {
|
||||
this.$nextTick(() => {
|
||||
window.scrollBy({ top: -Math.trunc(diff) })
|
||||
})
|
||||
}
|
||||
this.lastScrollPosition = getScrollPosition()
|
||||
})
|
||||
},
|
||||
scrollDown(options = {}) {
|
||||
const { behavior = 'auto', forceRead = false } = options
|
||||
this.$nextTick(() => {
|
||||
window.scrollTo({
|
||||
top: document.documentElement.scrollHeight,
|
||||
behavior,
|
||||
})
|
||||
})
|
||||
if (forceRead) {
|
||||
this.readChat()
|
||||
}
|
||||
},
|
||||
// Actions
|
||||
async readChat() {
|
||||
if (this.conversationId) return // Unsupported
|
||||
if (!this.maxId || document.hidden) {
|
||||
return
|
||||
}
|
||||
|
|
@ -225,11 +212,17 @@ const Chat = {
|
|||
this.lastReadMessageId = this.maxId
|
||||
this.newMessageCount = 0
|
||||
},
|
||||
bottomedOut(offset) {
|
||||
return isBottomedOut(offset)
|
||||
},
|
||||
reachedTop() {
|
||||
return window.scrollY <= 0
|
||||
scrollDown(options = {}) {
|
||||
const { behavior = 'auto', forceRead = false } = options
|
||||
this.$nextTick(() => {
|
||||
window.scrollTo({
|
||||
top: document.documentElement.scrollHeight,
|
||||
behavior,
|
||||
})
|
||||
})
|
||||
if (forceRead) {
|
||||
this.readChat()
|
||||
}
|
||||
},
|
||||
cullOlder() {
|
||||
const maxIndex = this.messages.length
|
||||
|
|
@ -248,44 +241,6 @@ const Chat = {
|
|||
|
||||
this.messages = this.messages.slice(minIndex, maxIndex)
|
||||
},
|
||||
cullOlderCheck() {
|
||||
window.setTimeout(() => {
|
||||
if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
||||
this.cullOlder()
|
||||
}
|
||||
}, 5000)
|
||||
},
|
||||
handleScroll: throttle(function () {
|
||||
if (!this.chat) {
|
||||
return
|
||||
}
|
||||
this.lastScrollPosition = getScrollPosition()
|
||||
|
||||
if (this.reachedTop()) {
|
||||
this.fetchChat({ maxId: this.minId })
|
||||
} else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
||||
this.jumpToBottomButtonVisible = false
|
||||
this.cullOlderCheck()
|
||||
if (this.newMessageCount > 0) {
|
||||
// Use a delay before marking as read to prevent situation where new messages
|
||||
// arrive just as you're leaving the view and messages that you didn't actually
|
||||
// get to see get marked as read.
|
||||
window.setTimeout(() => {
|
||||
// Don't mark as read if the element doesn't exist, user has left chat view
|
||||
if (this.$el) this.readChat()
|
||||
}, MARK_AS_READ_DELAY)
|
||||
}
|
||||
} else {
|
||||
this.jumpToBottomButtonVisible = true
|
||||
}
|
||||
}, 200),
|
||||
handleScrollUp(positionBeforeLoading) {
|
||||
const positionAfterLoading = getScrollPosition()
|
||||
|
||||
window.scrollTo({
|
||||
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
|
||||
})
|
||||
},
|
||||
clear() {
|
||||
this.messages = this.messages.filter((m) => m.error)
|
||||
this.messagesIndex = this.messages.reduce(
|
||||
|
|
@ -305,12 +260,31 @@ const Chat = {
|
|||
return
|
||||
}
|
||||
|
||||
const { data: messages } = await chatMessages({
|
||||
id: this.chat.id,
|
||||
maxId,
|
||||
sinceId: fetchLatest ? this.maxId : null,
|
||||
credentials: useOAuthStore().token,
|
||||
})
|
||||
let messages
|
||||
if (this.isConversation) {
|
||||
const [
|
||||
{ data: status },
|
||||
{ data: { ancestors, descendants } }
|
||||
] = await Promise.all([
|
||||
fetchStatus({
|
||||
id: this.statusId,
|
||||
credentials: useOAuthStore().token,
|
||||
}),
|
||||
fetchConversation({
|
||||
id: this.statusId,
|
||||
credentials: useOAuthStore().token,
|
||||
})
|
||||
])
|
||||
messages = [...ancestors, status, ...descendants]
|
||||
} else {
|
||||
const { data } = await chatMessages({
|
||||
id: this.chat.id,
|
||||
maxId,
|
||||
sinceId: fetchLatest ? this.maxId : null,
|
||||
credentials: useOAuthStore().token,
|
||||
})
|
||||
messages = data
|
||||
}
|
||||
|
||||
// Clear the current chat in case we're recovering from a ws connection loss.
|
||||
if (isFirstFetch) {
|
||||
|
|
@ -338,20 +312,22 @@ const Chat = {
|
|||
}
|
||||
},
|
||||
async startFetching() {
|
||||
try {
|
||||
const { data } = await getOrCreateChat({
|
||||
accountId: this.recipientId,
|
||||
credentials: useOAuthStore().token,
|
||||
})
|
||||
this.$store.commit('addNewUsers', [data.account])
|
||||
data.account = this.$store.getters.findUser(data.account.id)
|
||||
this.chat = data
|
||||
} catch (e) {
|
||||
console.error('Error creating or getting a chat', e)
|
||||
this.errorLoadingChat = true
|
||||
if (!this.isConversation) {
|
||||
try {
|
||||
const { data } = await getOrCreateChat({
|
||||
accountId: this.recipientId,
|
||||
credentials: useOAuthStore().token,
|
||||
})
|
||||
this.$store.commit('addNewUsers', [data.account])
|
||||
data.account = this.$store.getters.findUser(data.account.id)
|
||||
this.chat = data
|
||||
} catch (e) {
|
||||
console.error('Error creating or getting a chat', e)
|
||||
this.errorLoadingChat = true
|
||||
}
|
||||
}
|
||||
|
||||
if (this.chat) {
|
||||
if (this.isConversation || this.chat) {
|
||||
this.$nextTick(() => {
|
||||
this.scrollDown({ forceRead: true })
|
||||
})
|
||||
|
|
@ -365,33 +341,12 @@ const Chat = {
|
|||
)
|
||||
this.fetchChat({ isFirstFetch: true })
|
||||
},
|
||||
async deleteChatMessage({ chatId, messageId }) {
|
||||
if (!this.testMode)
|
||||
await deleteChatMessage({
|
||||
chatId,
|
||||
messageId,
|
||||
credentials: useOAuthStore().token,
|
||||
})
|
||||
|
||||
this.messages = this.messages.filter((m) => m.id !== messageId)
|
||||
delete this.messagesIndex[messageId]
|
||||
|
||||
if (this.maxId === messageId) {
|
||||
const lastMessage = maxBy(this.messages, 'id')
|
||||
this.maxId = lastMessage.id
|
||||
}
|
||||
|
||||
if (this.minId === messageId) {
|
||||
const firstMessage = minBy(this.messages, 'id')
|
||||
this.minId = firstMessage.id
|
||||
}
|
||||
},
|
||||
addMessages({ messages: newMessages }) {
|
||||
for (let i = 0; i < newMessages.length; i++) {
|
||||
const message = newMessages[i]
|
||||
|
||||
// Sanity check
|
||||
if (message.chat_id !== this.chat.id) {
|
||||
if (!this.isConversation && (message.chat_id !== this.chat.id)) {
|
||||
console.warn(
|
||||
`Chat message doesn't belong to current chat (id: ${this.chat.id})!!`,
|
||||
message,
|
||||
|
|
@ -428,14 +383,11 @@ const Chat = {
|
|||
}
|
||||
}
|
||||
},
|
||||
handleAttachmentPosting() {
|
||||
this.$nextTick(() => {
|
||||
this.handleResize()
|
||||
// When the posting form size changes because of a media attachment, we need an extra resize
|
||||
// to account for the potential delay in the DOM update.
|
||||
this.scrollDown({ forceRead: true })
|
||||
})
|
||||
goBack() {
|
||||
this.$router.back()
|
||||
},
|
||||
|
||||
// Optimistic posting (chats only)
|
||||
async sendMessage({ status, media, idempotencyKey }) {
|
||||
const params = {
|
||||
id: this.chat.id,
|
||||
|
|
@ -515,12 +467,122 @@ const Chat = {
|
|||
fakeMessage.pending = false
|
||||
}
|
||||
},
|
||||
goBack() {
|
||||
this.$router.push({
|
||||
name: 'chats',
|
||||
params: { username: this.currentUser.screen_name },
|
||||
|
||||
// Checks
|
||||
hasReachedTop() {
|
||||
return window.scrollY <= 0
|
||||
},
|
||||
cullOlderCheck() {
|
||||
if (this.conversationId) return
|
||||
window.setTimeout(() => {
|
||||
if (isBottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
||||
this.cullOlder()
|
||||
}
|
||||
}, 5000)
|
||||
},
|
||||
|
||||
// Event handlers
|
||||
onPosted(data) {
|
||||
this.explicitReplyStatus = null
|
||||
this.$router.push({ name: 'conversation2', params: { statusId: data.id } })
|
||||
},
|
||||
handleVisibilityChange() {
|
||||
this.$nextTick(() => {
|
||||
if (!document.hidden && isBottomedOut(BOTTOMED_OUT_OFFSET)) {
|
||||
this.scrollDown({ forceRead: true })
|
||||
}
|
||||
})
|
||||
},
|
||||
onFilesDropped() {
|
||||
this.$nextTick(() => {
|
||||
this.handleResize()
|
||||
})
|
||||
},
|
||||
handleResize(opts = {}) {
|
||||
// "Sticks" scroll to bottom instead of top, helps with OSK resizing the viewport
|
||||
const { delayed = false } = opts
|
||||
|
||||
if (delayed) {
|
||||
setTimeout(() => {
|
||||
this.handleResize({ ...opts, delayed: false })
|
||||
}, SAFE_RESIZE_TIME_OFFSET)
|
||||
return
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
const { offsetHeight = undefined } = getScrollPosition()
|
||||
const diff = offsetHeight - this.lastScrollPosition.offsetHeight
|
||||
if (diff !== 0 && !isBottomedOut()) {
|
||||
this.$nextTick(() => {
|
||||
window.scrollBy({ top: -Math.trunc(diff) })
|
||||
})
|
||||
}
|
||||
this.lastScrollPosition = getScrollPosition()
|
||||
})
|
||||
},
|
||||
handleScroll: throttle(function () {
|
||||
if (!this.chat) {
|
||||
return
|
||||
}
|
||||
this.lastScrollPosition = getScrollPosition()
|
||||
|
||||
if (this.hasReachedTop()) {
|
||||
this.fetchChat({ maxId: this.minId })
|
||||
} else if (isBottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
||||
this.jumpToBottomButtonVisible = false
|
||||
this.cullOlderCheck()
|
||||
if (this.newMessageCount > 0) {
|
||||
// Use a delay before marking as read to prevent situation where new messages
|
||||
// arrive just as you're leaving the view and messages that you didn't actually
|
||||
// get to see get marked as read.
|
||||
window.setTimeout(() => {
|
||||
// Don't mark as read if the element doesn't exist, user has left chat view
|
||||
if (this.$el) this.readChat()
|
||||
}, MARK_AS_READ_DELAY)
|
||||
}
|
||||
} else {
|
||||
this.jumpToBottomButtonVisible = true
|
||||
}
|
||||
}, 200),
|
||||
handleScrollUp(positionBeforeLoading) {
|
||||
const positionAfterLoading = getScrollPosition()
|
||||
|
||||
window.scrollTo({
|
||||
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
|
||||
})
|
||||
},
|
||||
handleAttachmentPosting() {
|
||||
this.$nextTick(() => {
|
||||
this.handleResize()
|
||||
// When the posting form size changes because of a media attachment, we need an extra resize
|
||||
// to account for the potential delay in the DOM update.
|
||||
this.scrollDown({ forceRead: true })
|
||||
})
|
||||
},
|
||||
|
||||
// Ugly
|
||||
// TODO move to ChatMessage
|
||||
async deleteChatMessage({ chatId, messageId }) {
|
||||
if (!this.testMode)
|
||||
await deleteChatMessage({
|
||||
chatId,
|
||||
messageId,
|
||||
credentials: useOAuthStore().token,
|
||||
})
|
||||
|
||||
this.messages = this.messages.filter((m) => m.id !== messageId)
|
||||
delete this.messagesIndex[messageId]
|
||||
|
||||
if (this.maxId === messageId) {
|
||||
const lastMessage = maxBy(this.messages, 'id')
|
||||
this.maxId = lastMessage.id
|
||||
}
|
||||
|
||||
if (this.minId === messageId) {
|
||||
const firstMessage = minBy(this.messages, 'id')
|
||||
this.minId = firstMessage.id
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue