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
|
|
@ -65,6 +65,15 @@ export default (store) => {
|
||||||
component: ConversationPage,
|
component: ConversationPage,
|
||||||
meta: { dontScroll: true },
|
meta: { dontScroll: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'conversation2',
|
||||||
|
path: '/conversation/:statusId',
|
||||||
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/chat_view/chat_view.vue'),
|
||||||
|
),
|
||||||
|
props: true,
|
||||||
|
beforeEnter: validateAuthenticatedRoute,
|
||||||
|
},
|
||||||
{ name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline },
|
{ name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline },
|
||||||
{
|
{
|
||||||
name: 'remote-user-profile-acct',
|
name: 'remote-user-profile-acct',
|
||||||
|
|
|
||||||
|
|
@ -75,14 +75,19 @@ const ChatMessage = {
|
||||||
// ChatMessage only has account_id while Status has full user data
|
// ChatMessage only has account_id while Status has full user data
|
||||||
return !!this.message.user
|
return !!this.message.user
|
||||||
},
|
},
|
||||||
author() {
|
authorId() {
|
||||||
const accountId = this.isStatus
|
return this.isStatus
|
||||||
? this.message.user.id
|
? this.message.user.id
|
||||||
: this.message.account_id
|
: this.message.account_id
|
||||||
|
},
|
||||||
return this.$store.getters.findUser(accountId)
|
author() {
|
||||||
|
return this.$store.getters.findUser(this.authorId)
|
||||||
},
|
},
|
||||||
isCurrentUser() {
|
isCurrentUser() {
|
||||||
|
// mini-hack/optimizaiton:
|
||||||
|
// - current user would always be in memory so if user is missing it's obviously not us
|
||||||
|
// - if anon views page then "us" pretty much doesn't exist
|
||||||
|
if (!this.author || !this.currentUser) return false
|
||||||
return this.author.id === this.currentUser.id
|
return this.author.id === this.currentUser.id
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,9 +74,10 @@
|
||||||
>
|
>
|
||||||
<UserPopover
|
<UserPopover
|
||||||
v-if="chatItem.isHead"
|
v-if="chatItem.isHead"
|
||||||
:user-id="author.id"
|
:user-id="authorId"
|
||||||
>
|
>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
|
v-if="author"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
:user="author"
|
:user="author"
|
||||||
/>
|
/>
|
||||||
|
|
@ -202,7 +203,7 @@
|
||||||
{{ ' ' }}
|
{{ ' ' }}
|
||||||
<router-link
|
<router-link
|
||||||
class="timeago faint"
|
class="timeago faint"
|
||||||
:to="{ name: 'conversation', params: { id: message.id } }"
|
:to="{ name: 'conversation2', params: { statusId: message.id } }"
|
||||||
>
|
>
|
||||||
<Timeago
|
<Timeago
|
||||||
:time="message.created_at"
|
:time="message.created_at"
|
||||||
|
|
|
||||||
|
|
@ -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 { mapState as mapPiniaState } from 'pinia'
|
||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
|
|
@ -28,6 +28,7 @@ import {
|
||||||
sendChatMessage,
|
sendChatMessage,
|
||||||
} from 'src/api/chats.js'
|
} from 'src/api/chats.js'
|
||||||
import { WSConnectionStatus } from 'src/api/websocket.js'
|
import { WSConnectionStatus } from 'src/api/websocket.js'
|
||||||
|
import { fetchConversation, fetchStatus } from 'src/api/public.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
|
import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
@ -52,6 +53,7 @@ const Chat = {
|
||||||
PostStatusForm,
|
PostStatusForm,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
statusId: String,
|
||||||
testMode: Boolean,
|
testMode: Boolean,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -65,6 +67,9 @@ const Chat = {
|
||||||
minId: undefined,
|
minId: undefined,
|
||||||
maxId: undefined,
|
maxId: undefined,
|
||||||
|
|
||||||
|
// Conversation stuff
|
||||||
|
explicitReplyStatus: null,
|
||||||
|
|
||||||
// Unread stuff
|
// Unread stuff
|
||||||
newMessageCount: 0,
|
newMessageCount: 0,
|
||||||
lastReadMessageId: null,
|
lastReadMessageId: null,
|
||||||
|
|
@ -108,6 +113,17 @@ const Chat = {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
computed: {
|
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() {
|
recipient() {
|
||||||
return this.chat?.account
|
return this.chat?.account
|
||||||
},
|
},
|
||||||
|
|
@ -123,7 +139,18 @@ const Chat = {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Conversation stuff
|
||||||
|
lastStatus() {
|
||||||
|
return this.messages[this.messages.length - 1]
|
||||||
|
},
|
||||||
|
replyStatus() {
|
||||||
|
return this.explicitReplyStatus ?? this.lastStatus
|
||||||
|
},
|
||||||
|
|
||||||
|
// Global Stuff
|
||||||
streamingEnabled() {
|
streamingEnabled() {
|
||||||
|
if (this.isConversation) return false // Unsupported
|
||||||
return (
|
return (
|
||||||
this.mergedConfig.useStreamingApi &&
|
this.mergedConfig.useStreamingApi &&
|
||||||
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||||
|
|
@ -142,13 +169,17 @@ const Chat = {
|
||||||
messages() {
|
messages() {
|
||||||
// We don't want to scroll to the bottom on a new message when the user is viewing older 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.
|
// 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(() => {
|
this.$nextTick(() => {
|
||||||
if (bottomedOutBeforeUpdate) {
|
if (bottomedOutBeforeUpdate) {
|
||||||
this.scrollDown()
|
this.scrollDown()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
async replyStatus(newVal) {
|
||||||
|
await nextTick() // wait for changes to propagate to postStatusForm
|
||||||
|
this.$refs.postStatusForm.update()
|
||||||
|
},
|
||||||
$route: function () {
|
$route: function () {
|
||||||
this.startFetching()
|
this.startFetching()
|
||||||
},
|
},
|
||||||
|
|
@ -159,53 +190,9 @@ const Chat = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onFilesDropped() {
|
// Actions
|
||||||
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()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async readChat() {
|
async readChat() {
|
||||||
|
if (this.conversationId) return // Unsupported
|
||||||
if (!this.maxId || document.hidden) {
|
if (!this.maxId || document.hidden) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -225,11 +212,17 @@ const Chat = {
|
||||||
this.lastReadMessageId = this.maxId
|
this.lastReadMessageId = this.maxId
|
||||||
this.newMessageCount = 0
|
this.newMessageCount = 0
|
||||||
},
|
},
|
||||||
bottomedOut(offset) {
|
scrollDown(options = {}) {
|
||||||
return isBottomedOut(offset)
|
const { behavior = 'auto', forceRead = false } = options
|
||||||
},
|
this.$nextTick(() => {
|
||||||
reachedTop() {
|
window.scrollTo({
|
||||||
return window.scrollY <= 0
|
top: document.documentElement.scrollHeight,
|
||||||
|
behavior,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (forceRead) {
|
||||||
|
this.readChat()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
cullOlder() {
|
cullOlder() {
|
||||||
const maxIndex = this.messages.length
|
const maxIndex = this.messages.length
|
||||||
|
|
@ -248,44 +241,6 @@ const Chat = {
|
||||||
|
|
||||||
this.messages = this.messages.slice(minIndex, maxIndex)
|
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() {
|
clear() {
|
||||||
this.messages = this.messages.filter((m) => m.error)
|
this.messages = this.messages.filter((m) => m.error)
|
||||||
this.messagesIndex = this.messages.reduce(
|
this.messagesIndex = this.messages.reduce(
|
||||||
|
|
@ -305,12 +260,31 @@ const Chat = {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: messages } = await chatMessages({
|
let messages
|
||||||
id: this.chat.id,
|
if (this.isConversation) {
|
||||||
maxId,
|
const [
|
||||||
sinceId: fetchLatest ? this.maxId : null,
|
{ data: status },
|
||||||
credentials: useOAuthStore().token,
|
{ 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.
|
// Clear the current chat in case we're recovering from a ws connection loss.
|
||||||
if (isFirstFetch) {
|
if (isFirstFetch) {
|
||||||
|
|
@ -338,20 +312,22 @@ const Chat = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async startFetching() {
|
async startFetching() {
|
||||||
try {
|
if (!this.isConversation) {
|
||||||
const { data } = await getOrCreateChat({
|
try {
|
||||||
accountId: this.recipientId,
|
const { data } = await getOrCreateChat({
|
||||||
credentials: useOAuthStore().token,
|
accountId: this.recipientId,
|
||||||
})
|
credentials: useOAuthStore().token,
|
||||||
this.$store.commit('addNewUsers', [data.account])
|
})
|
||||||
data.account = this.$store.getters.findUser(data.account.id)
|
this.$store.commit('addNewUsers', [data.account])
|
||||||
this.chat = data
|
data.account = this.$store.getters.findUser(data.account.id)
|
||||||
} catch (e) {
|
this.chat = data
|
||||||
console.error('Error creating or getting a chat', e)
|
} catch (e) {
|
||||||
this.errorLoadingChat = true
|
console.error('Error creating or getting a chat', e)
|
||||||
|
this.errorLoadingChat = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.chat) {
|
if (this.isConversation || this.chat) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.scrollDown({ forceRead: true })
|
this.scrollDown({ forceRead: true })
|
||||||
})
|
})
|
||||||
|
|
@ -365,33 +341,12 @@ const Chat = {
|
||||||
)
|
)
|
||||||
this.fetchChat({ isFirstFetch: true })
|
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 }) {
|
addMessages({ messages: newMessages }) {
|
||||||
for (let i = 0; i < newMessages.length; i++) {
|
for (let i = 0; i < newMessages.length; i++) {
|
||||||
const message = newMessages[i]
|
const message = newMessages[i]
|
||||||
|
|
||||||
// Sanity check
|
// Sanity check
|
||||||
if (message.chat_id !== this.chat.id) {
|
if (!this.isConversation && (message.chat_id !== this.chat.id)) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Chat message doesn't belong to current chat (id: ${this.chat.id})!!`,
|
`Chat message doesn't belong to current chat (id: ${this.chat.id})!!`,
|
||||||
message,
|
message,
|
||||||
|
|
@ -428,14 +383,11 @@ const Chat = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleAttachmentPosting() {
|
goBack() {
|
||||||
this.$nextTick(() => {
|
this.$router.back()
|
||||||
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 })
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Optimistic posting (chats only)
|
||||||
async sendMessage({ status, media, idempotencyKey }) {
|
async sendMessage({ status, media, idempotencyKey }) {
|
||||||
const params = {
|
const params = {
|
||||||
id: this.chat.id,
|
id: this.chat.id,
|
||||||
|
|
@ -515,12 +467,122 @@ const Chat = {
|
||||||
fakeMessage.pending = false
|
fakeMessage.pending = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
goBack() {
|
|
||||||
this.$router.push({
|
// Checks
|
||||||
name: 'chats',
|
hasReachedTop() {
|
||||||
params: { username: this.currentUser.screen_name },
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,4 +95,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reply-to-text {
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.2;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
margin-bottom: -0.5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,17 @@
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div class="title text-center">
|
<div class="title text-center">
|
||||||
|
<template v-if="isConversation">
|
||||||
|
<RichContent
|
||||||
|
v-if="messages[0]?.summary"
|
||||||
|
:html="messages[0].summary"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ $t('timeline.conversation') }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
<ChatTitle
|
<ChatTitle
|
||||||
|
v-else
|
||||||
:user="recipient"
|
:user="recipient"
|
||||||
:with-avatar="true"
|
:with-avatar="true"
|
||||||
/>
|
/>
|
||||||
|
|
@ -29,7 +39,10 @@
|
||||||
header-date
|
header-date
|
||||||
:messages="messages"
|
:messages="messages"
|
||||||
:pending-messages="pendingMessages"
|
:pending-messages="pendingMessages"
|
||||||
|
:replied-id="replyStatus?.id"
|
||||||
|
:focused-id="statusId"
|
||||||
@message-delete="deleteChatMessage"
|
@message-delete="deleteChatMessage"
|
||||||
|
@reply-requested="e => explicitReplyStatus = e"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref="footer"
|
ref="footer"
|
||||||
|
|
@ -50,27 +63,52 @@
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="auto-reply-to-section">
|
||||||
|
<div class="reply-to-text">
|
||||||
|
{{ explicitReplyStatus ? $t('status.reply_to_selected') : $t('status.reply_to_last') }}
|
||||||
|
<button
|
||||||
|
v-if="explicitReplyStatus"
|
||||||
|
class="button-default"
|
||||||
|
@click="explicitReplyStatus = null"
|
||||||
|
>
|
||||||
|
<FAIcon icon="times" />
|
||||||
|
{{ $t('general.cancel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<PostStatusForm
|
<PostStatusForm
|
||||||
:disable-subject="true"
|
ref="postStatusForm"
|
||||||
:disable-scope-selector="true"
|
:reply-to="replyStatus?.id"
|
||||||
:disable-notice="true"
|
:mentions-line="isConversation"
|
||||||
:disable-lock-warning="true"
|
mentions-line-read-only
|
||||||
:disable-polls="true"
|
:attentions="replyStatus?.attentions"
|
||||||
:disable-quotes="true"
|
:replied-user="replyStatus?.user"
|
||||||
:disable-sensitivity-checkbox="true"
|
:replied-subject="replyStatus?.summary"
|
||||||
:disable-submit="errorLoadingChat || !chat"
|
:replied-scope="replyStatus?.visibility"
|
||||||
:disable-preview="true"
|
|
||||||
:disable-draft="true"
|
disable-quotes
|
||||||
:optimistic-posting="true"
|
disable-notice
|
||||||
:post-handler="sendMessage"
|
disable-lock-warning
|
||||||
|
:disable-subject="!isConversation"
|
||||||
|
:disable-scope-selector="!isConversation"
|
||||||
|
:disable-polls="!isConversation"
|
||||||
|
:disable-sensitivity-checkbox="!isConversation"
|
||||||
|
:disable-preview="!isConversation"
|
||||||
|
:disable-draft="!isConversation"
|
||||||
|
|
||||||
|
:disable-submit="isConversation ? false : (errorLoadingChat || !chat)"
|
||||||
|
:optimistic-posting="!isConversation"
|
||||||
|
|
||||||
:submit-on-enter="!mobileLayout"
|
:submit-on-enter="!mobileLayout"
|
||||||
:preserve-focus="!mobileLayout"
|
:preserve-focus="!mobileLayout"
|
||||||
:auto-focus="!mobileLayout"
|
:auto-focus="!mobileLayout"
|
||||||
:placeholder="formPlaceholder"
|
:placeholder="formPlaceholder"
|
||||||
:file-limit="1"
|
:file-limit="isConversation ? null : 1"
|
||||||
max-height="160"
|
:max-height="160"
|
||||||
emoji-picker-placement="top"
|
emoji-picker-placement="top"
|
||||||
|
:post-handler="isConversation ? null : sendMessage"
|
||||||
@resize="handleResize"
|
@resize="handleResize"
|
||||||
|
@posted="onPosted"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,6 @@ const conversation = {
|
||||||
inlineDivePosition: null,
|
inlineDivePosition: null,
|
||||||
loadStatusError: null,
|
loadStatusError: null,
|
||||||
unsuspendibleIds: new Set(),
|
unsuspendibleIds: new Set(),
|
||||||
explicitReplyStatus: null,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
|
@ -130,12 +129,6 @@ const conversation = {
|
||||||
const maxDepth = this.mergedConfig.maxDepthInThread - 2
|
const maxDepth = this.mergedConfig.maxDepthInThread - 2
|
||||||
return maxDepth >= 1 ? maxDepth : 1
|
return maxDepth >= 1 ? maxDepth : 1
|
||||||
},
|
},
|
||||||
lastStatus() {
|
|
||||||
return this.conversation[this.conversation.length - 1]
|
|
||||||
},
|
|
||||||
replyStatus() {
|
|
||||||
return this.explicitReplyStatus ?? this.lastStatus
|
|
||||||
},
|
|
||||||
streamingEnabled() {
|
streamingEnabled() {
|
||||||
return (
|
return (
|
||||||
this.mergedConfig.useStreamingApi &&
|
this.mergedConfig.useStreamingApi &&
|
||||||
|
|
@ -152,10 +145,7 @@ const conversation = {
|
||||||
return this.displayStyle === 'tree'
|
return this.displayStyle === 'tree'
|
||||||
},
|
},
|
||||||
isLinearView() {
|
isLinearView() {
|
||||||
return this.displayStyle === 'linear'
|
return this.displayStyle !== 'tree'
|
||||||
},
|
|
||||||
isChatView() {
|
|
||||||
return this.displayStyle === 'chat'
|
|
||||||
},
|
},
|
||||||
shouldFadeAncestors() {
|
shouldFadeAncestors() {
|
||||||
return this.mergedConfig.conversationTreeFadeAncestors
|
return this.mergedConfig.conversationTreeFadeAncestors
|
||||||
|
|
@ -636,7 +626,6 @@ const conversation = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPosted(data) {
|
onPosted(data) {
|
||||||
this.explicitReplyStatus = null
|
|
||||||
if (this.isPage) {
|
if (this.isPage) {
|
||||||
this.$router.push({ name: 'conversation', params: { id: data.id } })
|
this.$router.push({ name: 'conversation', params: { id: data.id } })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="isLinearView || (isChatView && !isExpanded)"
|
v-else-if="isLinearView"
|
||||||
class="thread-body"
|
class="thread-body"
|
||||||
>
|
>
|
||||||
<article>
|
<article>
|
||||||
|
|
@ -202,52 +202,6 @@
|
||||||
/>
|
/>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-else-if="isChatView"
|
|
||||||
class="chat-view"
|
|
||||||
>
|
|
||||||
<ChatMessageList
|
|
||||||
:messages="conversation"
|
|
||||||
:replied-id="replyStatus?.id"
|
|
||||||
:focused-id="maybeFocused"
|
|
||||||
@reply-requested="e => explicitReplyStatus = e"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="isChatView && isExpanded && replyStatus"
|
|
||||||
class="chat-view-reply-form panel-footer -flexible-height"
|
|
||||||
>
|
|
||||||
<div class="auto-reply-to-section">
|
|
||||||
<center class="reply-to-text">
|
|
||||||
{{ explicitReplyStatus ? $t('status.reply_to_selected') : $t('status.reply_to_last') }}
|
|
||||||
<button
|
|
||||||
v-if="explicitReplyStatus"
|
|
||||||
class="button-default"
|
|
||||||
@click="explicitReplyStatus = null"
|
|
||||||
>
|
|
||||||
<FAIcon icon="times" />
|
|
||||||
{{ $t('general.cancel') }}
|
|
||||||
</button>
|
|
||||||
</center>
|
|
||||||
</div>
|
|
||||||
<PostStatusForm
|
|
||||||
class="reply-form"
|
|
||||||
:reply-to="replyStatus.id"
|
|
||||||
:attentions="replyStatus.attentions"
|
|
||||||
:replied-user="replyStatus.user"
|
|
||||||
:replied-sibject="replyStatus.summary"
|
|
||||||
:replied-scope="replyStatus.visibility"
|
|
||||||
:submit-on-enter="!mobileLayout"
|
|
||||||
:preserve-focus="!mobileLayout"
|
|
||||||
:auto-focus="!mobileLayout"
|
|
||||||
disable-quotes
|
|
||||||
disable-lock-warning
|
|
||||||
disable-notice
|
|
||||||
mentions-line
|
|
||||||
mentions-line-read-only
|
|
||||||
@posted="onPosted"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -588,6 +588,12 @@ const PostStatusForm = {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// Composing
|
// Composing
|
||||||
|
update() {
|
||||||
|
Object.entries(this.defaultNewStatus).forEach(([key, value]) => {
|
||||||
|
if (key === 'status') return
|
||||||
|
this.newStatus[key] = value
|
||||||
|
})
|
||||||
|
},
|
||||||
onMentionsLineUpdate(e) {
|
onMentionsLineUpdate(e) {
|
||||||
if (this.mentionsLineReadOnly) return
|
if (this.mentionsLineReadOnly) return
|
||||||
// TODO
|
// TODO
|
||||||
|
|
|
||||||
|
|
@ -231,11 +231,12 @@
|
||||||
>
|
>
|
||||||
<scope-selector
|
<scope-selector
|
||||||
v-if="!disableVisibilitySelector"
|
v-if="!disableVisibilitySelector"
|
||||||
|
ref="scopeSelector"
|
||||||
:show-all="showAllScopes"
|
:show-all="showAllScopes"
|
||||||
:user-default="userDefaultScope"
|
:user-default="userDefaultScope"
|
||||||
:original-scope="repliedScope"
|
:original-scope="newStatus.visibility"
|
||||||
:initial-scope="newStatus.visibility"
|
:initial-scope="newStatus.visibility"
|
||||||
:on-scope-change="changeVis"
|
@change="changeVis"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -57,24 +57,6 @@
|
||||||
/> {{ $t('settings.conversation_display_linear_quick') }}
|
/> {{ $t('settings.conversation_display_linear_quick') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-item dropdown-item -icon-double">
|
|
||||||
<button
|
|
||||||
class="main-button"
|
|
||||||
:aria-checked="conversationDisplay === 'chat'"
|
|
||||||
role="menuitemradio"
|
|
||||||
@click="conversationDisplay = 'chat'"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="input menu-checkbox -radio"
|
|
||||||
:class="{ 'menu-checkbox-checked': conversationDisplay === 'chat' }"
|
|
||||||
:aria-hidden="true"
|
|
||||||
/><FAIcon
|
|
||||||
icon="message"
|
|
||||||
:aria-hidden="true"
|
|
||||||
fixed-width
|
|
||||||
/> {{ $t('settings.conversation_display_chat_quick') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
role="separator"
|
role="separator"
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,13 @@ const ScopeSelector = {
|
||||||
required: false,
|
required: false,
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
onScopeChange: {
|
|
||||||
required: true,
|
|
||||||
type: Function,
|
|
||||||
},
|
|
||||||
unstyled: {
|
unstyled: {
|
||||||
required: false,
|
required: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['change'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
currentScope: this.initialScope,
|
currentScope: this.initialScope,
|
||||||
|
|
@ -84,9 +81,14 @@ const ScopeSelector = {
|
||||||
},
|
},
|
||||||
changeVis(scope) {
|
changeVis(scope) {
|
||||||
this.currentScope = scope
|
this.currentScope = scope
|
||||||
this.onScopeChange && this.onScopeChange(scope)
|
this.$emit('change', scope)
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
originalScope(newVal) {
|
||||||
|
this.currentScope = newVal
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ScopeSelector
|
export default ScopeSelector
|
||||||
|
|
|
||||||
|
|
@ -801,8 +801,6 @@
|
||||||
"tree_fade_ancestors": "Display ancestors of the current status in faint text",
|
"tree_fade_ancestors": "Display ancestors of the current status in faint text",
|
||||||
"conversation_display_linear": "Linear-style",
|
"conversation_display_linear": "Linear-style",
|
||||||
"conversation_display_linear_quick": "Linear view",
|
"conversation_display_linear_quick": "Linear view",
|
||||||
"conversation_display_chat": "Chat-style",
|
|
||||||
"conversation_display_chat_quick": "Chat view",
|
|
||||||
"conversation_other_replies_button": "Show the \"other replies\" button",
|
"conversation_other_replies_button": "Show the \"other replies\" button",
|
||||||
"conversation_other_replies_button_below": "Below statuses",
|
"conversation_other_replies_button_below": "Below statuses",
|
||||||
"conversation_other_replies_button_inside": "Inside statuses",
|
"conversation_other_replies_button_inside": "Inside statuses",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue