remove chat mode from thread display settings, instead use dedicated page for chat convos

This commit is contained in:
Henry Jameson 2026-07-24 16:22:56 +03:00
commit c6ac897802
13 changed files with 303 additions and 249 deletions

View file

@ -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',

View file

@ -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
}, },

View file

@ -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"

View file

@ -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
}
},
}, },
} }

View file

@ -95,4 +95,11 @@
} }
} }
} }
.reply-to-text {
text-align: center;
line-height: 1.2;
padding-top: 0.5em;
margin-bottom: -0.5em;
}
} }

View file

@ -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>

View file

@ -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 } })
} }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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",