diff --git a/CHANGELOG.md b/CHANGELOG.md index d3b899d25..1eb5a9cb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,50 +3,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## 2.11 -### Added -- Initial MFM rendering support -- Button to remove all drafts -- Option to remove forced aspect ratio for user profiles (requested) -- Showing user tags (MRF policies for user + custom if present) -- Version information now is also in about page -- Mention autosuggest now sorts by recent activity -- Non-square emoji support (toggleable by user) -- Displaying other user's backgrounds (if supported by BE) -- Add quoting by URL and in replies -- Settings synchronization -- User highlight synchronization -- User administration + post scope/sensitivity admin change support - -### Changed -- Migrated to Vite 8 and optimized our imports, more stuff is loaded on-demand, reducing the initial load time and transfer size -- Overall improved spacing in status action buttons and post form -- Logout confirm button is now dangerous -- Reply/quote now is a radio group and wraps, fixes overflow on languages where labels are too wide -- Personal note input is now bigger -- Moved "edit pinned" to the bottom for status action buttons. -- Dots status action button drops down instead of up to avoid hiding the action buttons -- Improved attachment description (alt text) input and display - -### Fixed -- Fix HTML attribute parsing for escaped quotes -- Fix emojis breaking user bio/description editing -- Navbar wide logo cropping search input -- Danger buttons being too bright -- User background upload failure no longer breaks new uploads + displays an error -- Importing theme from old theme editor -- Removed duplicate federationpolicy entry in admin tab -- Repeater name overflowing content -- Reply popover is now shown if replied-to status is muted -- Second language input not having header -- Post form's bottom left buttons not showing their toggled state -- Some font overrides not working -- Popovers opening outside of window's boundaries -- Occasional blank page when showing new posts -- Fixed status action mute hiding itself on click -- Fix reply form crash when quote-reply settings are unavailable - - ## 2.10.1 ### Fixed - fixed being unable to set actor type from profile page diff --git a/changelog.d/api-refactor.skip b/changelog.d/api-refactor.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/attrs-parsing.fix b/changelog.d/attrs-parsing.fix new file mode 100644 index 000000000..e36e59a86 --- /dev/null +++ b/changelog.d/attrs-parsing.fix @@ -0,0 +1 @@ +Fix HTML attribute parsing for escaped quotes \ No newline at end of file diff --git a/changelog.d/ci-pr-uploads-removal.skip b/changelog.d/ci-pr-uploads-removal.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/fast.change b/changelog.d/fast.change new file mode 100644 index 000000000..1f0a89092 --- /dev/null +++ b/changelog.d/fast.change @@ -0,0 +1 @@ +Migrated to Vite 8 and optimized our imports, more stuff is loaded on-demand, reducing the initial load time and transfer size diff --git a/changelog.d/fix-emojis-breaking-bio.fix b/changelog.d/fix-emojis-breaking-bio.fix new file mode 100644 index 000000000..62a607d8a --- /dev/null +++ b/changelog.d/fix-emojis-breaking-bio.fix @@ -0,0 +1 @@ +Fix emojis breaking user bio/description editing diff --git a/changelog.d/instance-store-migration.skip b/changelog.d/instance-store-migration.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/mfm.add b/changelog.d/mfm.add new file mode 100644 index 000000000..b5d320955 --- /dev/null +++ b/changelog.d/mfm.add @@ -0,0 +1 @@ +Initial MFM rendering support diff --git a/changelog.d/minor.add b/changelog.d/minor.add new file mode 100644 index 000000000..5f4934173 --- /dev/null +++ b/changelog.d/minor.add @@ -0,0 +1,6 @@ +button to remove all drafts +option to remove forced aspect ratio for user profiles (requested) +showing user tags (mrf policies for user + custom if present) +version information now is also in about page +mention autosuggest now sorts by recent activity +non-square emoji support (toggleable by user) diff --git a/changelog.d/minor.change b/changelog.d/minor.change new file mode 100644 index 000000000..979d36955 --- /dev/null +++ b/changelog.d/minor.change @@ -0,0 +1,7 @@ +overall improved spacings in status action buttons and post form +logout confirm button is now dangerous +reply/quote now is a radio group and wraps, fixes overflow on languages where labels are too wide +personal note input is now bigger +moved "edit pinned" to the bottom for status action buttons. +dots status action button drops down instead of up to avoid hiding the action buttons +improved attachment description (alt text) input and display diff --git a/changelog.d/minor.fix b/changelog.d/minor.fix new file mode 100644 index 000000000..420836364 --- /dev/null +++ b/changelog.d/minor.fix @@ -0,0 +1,12 @@ +navbar wide logo cropping search input +danger buttons being too bright +user background upload failure no longer breaks new uploads + displays an error +importing theme from old theme editor +removed duplicate federationpolicy entry in admin tab +repeater name overflowing content +reply popover is now shown if replied-to status is muted +second language input not having header +post form's bottom left buttons not showing their toggled state +some font overrides not working +popovers opening outside of window's boundaries +occasional blank page when showing new posts diff --git a/changelog.d/more-fixes.skip b/changelog.d/more-fixes.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/mute-dropdown.fix b/changelog.d/mute-dropdown.fix new file mode 100644 index 000000000..33f12a571 --- /dev/null +++ b/changelog.d/mute-dropdown.fix @@ -0,0 +1 @@ +Fixed status action mute hiding itself on click diff --git a/changelog.d/profilebg.add b/changelog.d/profilebg.add new file mode 100644 index 000000000..a2c79074a --- /dev/null +++ b/changelog.d/profilebg.add @@ -0,0 +1 @@ +displaying other user's backgrounds (if supported by BE) diff --git a/changelog.d/quote-by-url.add b/changelog.d/quote-by-url.add new file mode 100644 index 000000000..ef401f93c --- /dev/null +++ b/changelog.d/quote-by-url.add @@ -0,0 +1 @@ +Add quoting by URL and in replies diff --git a/changelog.d/reply-quote-config.fix b/changelog.d/reply-quote-config.fix new file mode 100644 index 000000000..b6ac4e5e9 --- /dev/null +++ b/changelog.d/reply-quote-config.fix @@ -0,0 +1 @@ +Fix reply form crash when quote-reply settings are unavailable diff --git a/changelog.d/sync-config.add b/changelog.d/sync-config.add new file mode 100644 index 000000000..76a5a1cca --- /dev/null +++ b/changelog.d/sync-config.add @@ -0,0 +1,2 @@ +settings synchronization +user highlight synchronization diff --git a/changelog.d/user-management.add b/changelog.d/user-management.add new file mode 100644 index 000000000..ccd217f19 --- /dev/null +++ b/changelog.d/user-management.add @@ -0,0 +1 @@ +User administration + post scope/sensitivity admin change support diff --git a/changelog.d/woodpecker-pr-pipeline.skip b/changelog.d/woodpecker-pr-pipeline.skip new file mode 100644 index 000000000..e69de29bb diff --git a/package.json b/package.json index 1d94ac22f..f594d7a99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pleroma_fe", - "version": "2.11.0", + "version": "2.10.1", "description": "Pleroma frontend, the default frontend of Pleroma social network server", "author": "Pleroma contributors ", "private": false, diff --git a/src/api/chats.js b/src/api/chats.js index 5d766dec4..114038e52 100644 --- a/src/api/chats.js +++ b/src/api/chats.js @@ -1,9 +1,6 @@ import { paramsString, promisedRequest } from './helpers.js' -import { - parseChat, - parseChatMessage, -} from 'src/services/entity_normalizer/entity_normalizer.service.js' +import { parseChat } from 'src/services/entity_normalizer/entity_normalizer.service.js' const PLEROMA_CHATS_URL = '/api/v1/pleroma/chats' const PLEROMA_CHAT_URL = (id) => `/api/v1/pleroma/chats/by-account-id/${id}` @@ -18,7 +15,7 @@ export const chats = ({ credentials }) => url: PLEROMA_CHATS_URL, credentials, }).then(({ data }) => ({ - data: data.map(parseChat).filter((c) => c), + chatList: data.map(parseChat).filter((c) => c), })) export const getOrCreateChat = ({ accountId, credentials }) => @@ -26,7 +23,7 @@ export const getOrCreateChat = ({ accountId, credentials }) => url: PLEROMA_CHAT_URL(accountId), method: 'POST', credentials, - }).then(({ data }) => ({ data: parseChat(data) })) + }) export const chatMessages = ({ id, @@ -39,9 +36,7 @@ export const chatMessages = ({ url: PLEROMA_CHAT_MESSAGES_URL(id, { maxId, sinceId, limit }), method: 'GET', credentials, - }).then(({ data }) => ({ - data: data.map(parseChatMessage).filter((c) => c), - })) + }) } export const sendChatMessage = ({ @@ -71,9 +66,7 @@ export const sendChatMessage = ({ payload, credentials, headers, - }).then(({ data }) => ({ - data: parseChatMessage(data), - })) + }) } export const readChat = ({ id, lastReadId, credentials }) => diff --git a/src/boot/routes.js b/src/boot/routes.js index 0dfb1e744..d50baab04 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -293,7 +293,7 @@ export default (store) => { name: 'chat', path: '/users/:username/chats/:recipient_id', component: defineAsyncComponent( - () => import('src/components/chat_view/chat_view.vue'), + () => import('src/components/chat/chat.vue'), ), meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute, diff --git a/src/components/chat_view/chat_view.js b/src/components/chat/chat.js similarity index 51% rename from src/components/chat_view/chat_view.js rename to src/components/chat/chat.js index ee72d592f..ca7a025ec 100644 --- a/src/components/chat_view/chat_view.js +++ b/src/components/chat/chat.js @@ -1,11 +1,11 @@ -import { maxBy, minBy, sortBy, throttle } from 'lodash' +import { throttle } from 'lodash' import { mapState as mapPiniaState } from 'pinia' -import { nextTick } from 'vue' -import { mapState } from 'vuex' +import { mapGetters, mapState } from 'vuex' -import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue' +import ChatMessage from 'src/components/chat_message/chat_message.vue' import ChatTitle from 'src/components/chat_title/chat_title.vue' import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' +import chatService from '../../services/chat_service/chat_service.js' import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js' import { promiseInterval } from '../../services/promise_interval/promise_interval.js' import { @@ -15,16 +15,13 @@ import { isScrollable, } from './chat_layout_utils.js' -import { useChatsStore } from 'src/stores/chats.js' import { useInterfaceStore } from 'src/stores/interface.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useOAuthStore } from 'src/stores/oauth.js' import { chatMessages, - deleteChatMessage, getOrCreateChat, - readChat, sendChatMessage, } from 'src/api/chats.js' import { WSConnectionStatus } from 'src/api/websocket.js' @@ -40,46 +37,23 @@ const SAFE_RESIZE_TIME_OFFSET = 100 const MARK_AS_READ_DELAY = 1500 const MAX_RETRIES = 10 -const isConfirmation = (storage, message) => { - if (!message.idempotency_key) return - return storage.idempotencyKeyIndex[message.idempotency_key] -} - const Chat = { components: { - ChatMessageList, + ChatMessage, ChatTitle, PostStatusForm, }, - props: { - testMode: Boolean, - }, data() { return { - // Main info - chat: null, - messages: [], - messagesIndex: {}, - pendingMessages: [], - pendingMessagesIndex: {}, - minId: undefined, - maxId: undefined, - - // Unread stuff - newMessageCount: 0, - lastReadMessageId: null, - lastScrollPosition: {}, jumpToBottomButtonVisible: false, - - // Internal network stuff - fetcher: null, + hoveredMessageChainId: undefined, + lastScrollPosition: {}, + scrollableContainerHeight: '100%', errorLoadingChat: false, messageRetriers: {}, - idempotencyKeyIndex: {}, } }, created() { - if (this.testMode) return this.startFetching() window.addEventListener('resize', this.handleResize) }, @@ -106,10 +80,11 @@ const Chat = { this.handleVisibilityChange, false, ) + this.$store.dispatch('clearCurrentChat') }, computed: { recipient() { - return this.chat?.account + return this.currentChat && this.currentChat.account }, recipientId() { return this.$route.params.recipient_id @@ -123,23 +98,37 @@ const Chat = { return '' } }, + chatViewItems() { + return chatService.getView(this.currentChatMessageService) + }, + newMessageCount() { + return ( + this.currentChatMessageService && + this.currentChatMessageService.newMessageCount + ) + }, streamingEnabled() { return ( this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED ) }, + ...mapGetters([ + 'currentChat', + 'currentChatMessageService', + 'findOpenedChatByRecipientId', + ]), + ...mapPiniaState(useMergedConfigStore, ['mergedConfig']), ...mapPiniaState(useInterfaceStore, { mobileLayout: (store) => store.layoutType === 'mobile', }), - ...mapPiniaState(useMergedConfigStore, ['mergedConfig']), ...mapState({ mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus, currentUser: (state) => state.users.currentUser, }), }, watch: { - messages() { + chatViewItems() { // 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) @@ -159,6 +148,10 @@ const Chat = { }, }, methods: { + // Used to animate the avatar near the first message of the message chain when any message belonging to the chain is hovered + onMessageHover({ isHovered, messageChainId }) { + this.hoveredMessageChainId = isHovered ? messageChainId : undefined + }, onFilesDropped() { this.$nextTick(() => { this.handleResize() @@ -205,25 +198,22 @@ const Chat = { this.readChat() } }, - async readChat() { - if (!this.maxId || document.hidden) { + readChat() { + if ( + !( + this.currentChatMessageService && this.currentChatMessageService.maxId + ) + ) { return } - const lastReadId = this.maxId - const isNewMessage = this.lastReadMessageId !== lastReadId - - if (!isNewMessage) return - - if (!this.testMode) - await readChat({ - id: this.chat.id, - lastReadId, - credentials: useOAuthStore().token, - }) - - useChatsStore().readChat(this.chat.id) - this.lastReadMessageId = this.maxId - this.newMessageCount = 0 + if (document.hidden) { + return + } + const lastReadId = this.currentChatMessageService.maxId + this.$store.dispatch('readChat', { + id: this.currentChat.id, + lastReadId, + }) }, bottomedOut(offset) { return isBottomedOut(offset) @@ -231,38 +221,24 @@ const Chat = { reachedTop() { return window.scrollY <= 0 }, - cullOlder() { - const maxIndex = this.messages.length - const minIndex = maxIndex - 50 - if (maxIndex <= 50) return - - this.messages = sortBy(this.messages, ['id']) - this.minId = this.messages[minIndex].id - - for (const message of this.messages) { - if (message.id < this.minId) { - delete this.messagesIndex[message.id] - delete this.idempotencyKeyIndex[message.idempotency_key] - } - } - - this.messages = this.messages.slice(minIndex, maxIndex) - }, cullOlderCheck() { window.setTimeout(() => { if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) { - this.cullOlder() + this.$store.dispatch( + 'cullOlderMessages', + this.currentChatMessageService.chatId, + ) } }, 5000) }, handleScroll: throttle(function () { - if (!this.chat) { + this.lastScrollPosition = getScrollPosition() + if (!this.currentChat) { return } - this.lastScrollPosition = getScrollPosition() if (this.reachedTop()) { - this.fetchChat({ maxId: this.minId }) + this.fetchChat({ maxId: this.currentChatMessageService.minId }) } else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) { this.jumpToBottomButtonVisible = false this.cullOlderCheck() @@ -281,151 +257,85 @@ const Chat = { }, 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( - (acc, m) => ({ - ...acc, - [m.id]: m, - }), - {}, - ) - this.newMessageCount = 0 - this.lastReadMessageId = null - this.minId = undefined - this.maxId = undefined - }, - async fetchChat({ isFirstFetch = false, fetchLatest = false, maxId }) { + fetchChat({ isFirstFetch = false, fetchLatest = false, maxId }) { + const chatMessageService = this.currentChatMessageService + if (!chatMessageService) { + return + } if (fetchLatest && this.streamingEnabled) { return } - const { data: messages } = await chatMessages({ - id: this.chat.id, - maxId, - sinceId: fetchLatest ? this.maxId : null, - credentials: useOAuthStore().token, - }) - - // Clear the current chat in case we're recovering from a ws connection loss. - if (isFirstFetch) { - this.clear() - } - - const positionBeforeUpdate = getScrollPosition() - this.addMessages({ messages }) - - await nextTick() - + const chatId = chatMessageService.chatId const fetchOlderMessages = !!maxId - if (fetchOlderMessages) { - this.handleScrollUp(positionBeforeUpdate) - } + const sinceId = fetchLatest && chatMessageService.maxId - // In vertical screens, the first batch of fetched messages may not always take the - // full height of the scrollable container. - // If this is the case, we want to fetch the messages until the scrollable container - // is fully populated so that the user has the ability to scroll up and load the history. - if (!isScrollable() && messages.length > 0) { - this.fetchChat({ - maxId: this.minId, - }) - } + return chatMessages({ + id: chatId, + maxId, + sinceId, + credentials: useOAuthStore().token, + }).then(({ data: messages }) => { + // Clear the current chat in case we're recovering from a ws connection loss. + if (isFirstFetch) { + chatService.clear(chatMessageService) + } + + const positionBeforeUpdate = getScrollPosition() + this.$store + .dispatch('addChatMessages', { chatId, messages }) + .then(() => { + this.$nextTick(() => { + if (fetchOlderMessages) { + this.handleScrollUp(positionBeforeUpdate) + } + + // In vertical screens, the first batch of fetched messages may not always take the + // full height of the scrollable container. + // If this is the case, we want to fetch the messages until the scrollable container + // is fully populated so that the user has the ability to scroll up and load the history. + if (!isScrollable() && messages.length > 0) { + this.fetchChat({ + maxId: this.currentChatMessageService.minId, + }) + } + }) + }) + }) }, async startFetching() { - try { - const { data } = await getOrCreateChat({ - accountId: this.recipientId, - credentials: useOAuthStore().token, - }) - this.chat = data - } catch (e) { - console.error('Error creating or getting a chat', e) - this.errorLoadingChat = true + let chat = this.findOpenedChatByRecipientId(this.recipientId) + if (!chat) { + try { + const { data } = await getOrCreateChat({ + accountId: this.recipientId, + credentials: useOAuthStore().token, + }) + chat = data + } catch (e) { + console.error('Error creating or getting a chat', e) + this.errorLoadingChat = true + } } - - if (this.chat) { + if (chat) { this.$nextTick(() => { this.scrollDown({ forceRead: true }) }) + this.$store.dispatch('addOpenedChat', { chat }) this.doStartFetching() } }, doStartFetching() { - this.fetcher = promiseInterval( - () => this.fetchChat({ fetchLatest: true }), - 5000, - ) + this.$store.dispatch('startFetchingCurrentChat', { + fetcher: () => + promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000), + }) 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) { - console.warn( - `Chat message doesn't belong to current chat (id: ${this.chat.id})!!`, - message, - ) - return - } - - // Clear any known pending messages - if (message.idempotency_key) { - if (this.pendingMessagesIndex[message.idempotencyKeyIndex]) { - delete this.pendingMessagesIndex[message.idempotencyKeyIndex] - this.pendingMessages = this.pendingMessages.filter( - ({ idempotency_key }) => - idempotency_key !== message.idempotency_key, - ) - } - } - - if (!this.minId || (!message.pending && message.id < this.minId)) { - this.minId = message.id - } - - if (!this.maxId || message.id > this.maxId) { - this.maxId = message.id - } - - if (!this.messagesIndex[message.id] && !isConfirmation(this, message)) { - if (this.lastReadMessageId < message.id) { - this.newMessageCount++ - } - this.messagesIndex[message.id] = message - this.messages.push(this.messagesIndex[message.id]) - this.idempotencyKeyIndex[message.idempotency_key] = true - } - } - }, handleAttachmentPosting() { this.$nextTick(() => { this.handleResize() @@ -434,9 +344,9 @@ const Chat = { this.scrollDown({ forceRead: true }) }) }, - async sendMessage({ status, media, idempotencyKey }) { + sendMessage({ status, media, idempotencyKey }) { const params = { - id: this.chat.id, + id: this.currentChat.id, content: status, idempotencyKey, } @@ -447,71 +357,69 @@ const Chat = { const fakeMessage = buildFakeMessage({ attachments: media, - chatId: this.chat.id, + chatId: this.currentChat.id, content: status, userId: this.currentUser.id, idempotencyKey, }) - this.pendingMessages.push(fakeMessage) - this.pendingMessagesIndex[idempotencyKey] = fakeMessage - - this.handleAttachmentPosting() + this.$store + .dispatch('addChatMessages', { + chatId: this.currentChat.id, + messages: [fakeMessage], + }) + .then(() => { + this.handleAttachmentPosting() + }) return this.doSendMessage({ params, + fakeMessage, retriesLeft: MAX_RETRIES, }) }, - async doSendMessage({ params, retriesLeft = MAX_RETRIES }) { + doSendMessage({ params, fakeMessage, retriesLeft = MAX_RETRIES }) { if (retriesLeft <= 0) return - try { - const { data } = await sendChatMessage({ - ...params, - credentials: useOAuthStore().token, + sendChatMessage({ + ...params, + credentials: useOAuthStore().token, + }) + .then(({ data }) => { + this.$store.dispatch('addChatMessages', { + chatId: this.currentChat.id, + updateMaxId: false, + messages: [{ ...data, fakeId: fakeMessage.id }], + }) + + return data + }) + .catch((error) => { + console.error('Error sending message', error) + this.$store.dispatch('handleMessageError', { + chatId: this.currentChat.id, + fakeId: fakeMessage.id, + isRetry: retriesLeft !== MAX_RETRIES, + }) + if ( + (error.statusCode >= 500 && error.statusCode < 600) || + error.message === 'Failed to fetch' + ) { + this.messageRetriers[fakeMessage.id] = setTimeout( + () => { + this.doSendMessage({ + params, + fakeMessage, + retriesLeft: retriesLeft - 1, + }) + }, + 1000 * 2 ** (MAX_RETRIES - retriesLeft), + ) + } + return {} }) - this.addMessages({ - messages: [{ ...data }], - }) - } catch (error) { - if ( - error.name !== 'StatusCodeError' || - error.message === 'Failed to fetch' - ) - throw error - console.error('Error sending message', error) - - this.handleMessageError({ - chatId: this.chat.id, - idempotencyKey: params.idempotencyKey, - isRetry: retriesLeft !== MAX_RETRIES, - }) - - if ( - (error.statusCode >= 500 && error.statusCode < 600) || - error.message === 'Failed to fetch' - ) { - this.messageRetriers[params.idempotencyKey] = setTimeout( - () => { - this.doSendMessage({ - params, - retriesLeft: retriesLeft - 1, - }) - }, - 1000 * 2 ** (MAX_RETRIES - retriesLeft), - ) - } - } - }, - handleMessageError(idempotencyKey, isRetry) { - const fakeMessage = this.pendingMessagesIndex[idempotencyKey] - - if (fakeMessage) { - fakeMessage.error = true - fakeMessage.pending = false - } + return Promise.resolve(fakeMessage) }, goBack() { this.$router.push({ diff --git a/src/components/chat_view/chat_view.scss b/src/components/chat/chat.scss similarity index 100% rename from src/components/chat_view/chat_view.scss rename to src/components/chat/chat.scss diff --git a/src/components/chat_message_list/chat_message_list.style.js b/src/components/chat/chat.style.js similarity index 76% rename from src/components/chat_message_list/chat_message_list.style.js rename to src/components/chat/chat.style.js index 7424fa9c8..55cf657c2 100644 --- a/src/components/chat_message_list/chat_message_list.style.js +++ b/src/components/chat/chat.style.js @@ -1,11 +1,10 @@ export default { name: 'Chat', - selector: '.ChatMessageList', + selector: '.chat-message-list', validInnerComponents: ['Text', 'Link', 'Icon', 'Avatar', 'ChatMessage'], defaultRules: [ { directives: { - backgroundNoCssColor: 'yes', background: '--bg', blur: '5px', }, diff --git a/src/components/chat_view/chat_view.vue b/src/components/chat/chat.vue similarity index 69% rename from src/components/chat_view/chat_view.vue rename to src/components/chat/chat.vue index 0255347a9..cedbdce69 100644 --- a/src/components/chat_view/chat_view.vue +++ b/src/components/chat/chat.vue @@ -25,12 +25,29 @@ /> - +
+ +
+
+ {{ $t('chats.error_loading_chat') }} +
+
+
+ new Set(store.prefsStorage.collections.pinnedNavItems).has('chats'), + }), shouldConfirmLogout() { return useMergedConfigStore().mergedConfig.modalOnLogout }, closingDrawerMarksAsSeen() { return useMergedConfigStore().mergedConfig.closingDrawerMarksAsSeen }, - ...mapState(useAnnouncementsStore, ['unreadAnnouncementCount']), - ...mapState(useMergedConfigStore, { - pinnedItems: (store) => - new Set(store.prefsStorage.collections.pinnedNavItems).has('chats'), - }), - ...mapState(useChatsStore, ['unreadChatsCount']), + ...mapGetters(['unreadChatCount']), }, methods: { toggleMobileSidebar() { diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue index 8cdb70a1a..743b7deb0 100644 --- a/src/components/mobile_nav/mobile_nav.vue +++ b/src/components/mobile_nav/mobile_nav.vue @@ -19,7 +19,7 @@ icon="bars" />
diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index 5a4a78ec4..ae6264217 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -1,5 +1,5 @@ import { mapState as mapPiniaState } from 'pinia' -import { mapState } from 'vuex' +import { mapGetters, mapState } from 'vuex' import BookmarkFoldersMenuContent from 'src/components/bookmark_folders_menu/bookmark_folders_menu_content.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' @@ -10,7 +10,6 @@ import NavigationEntry from 'src/components/navigation/navigation_entry.vue' import NavigationPins from 'src/components/navigation/navigation_pins.vue' import { useAnnouncementsStore } from 'src/stores/announcements' -import { useChatsStore } from 'src/stores/chats.js' import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' @@ -132,7 +131,6 @@ const NavPanel = { currentUser: (state) => state.users.currentUser, followRequestCount: (state) => state.api.followRequests.length, }), - ...mapPiniaState(useChatsStore, ['unreadChatsCount']), timelinesItems() { return filterNavigation( Object.entries({ ...TIMELINES }) @@ -164,6 +162,7 @@ const NavPanel = { }, ) }, + ...mapGetters(['unreadChatCount']), }, } diff --git a/src/components/navigation/navigation.js b/src/components/navigation/navigation.js index 39fa2c993..66fb0d347 100644 --- a/src/components/navigation/navigation.js +++ b/src/components/navigation/navigation.js @@ -76,7 +76,7 @@ export const ROOT_ITEMS = { icon: 'comments', label: 'nav.chats', badgeStyle: 'notification', - badgeGetter: 'unreadChatsCount', + badgeGetter: 'unreadChatCount', criteria: ['chats'], }, friendRequests: { diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index 393aef639..49349b127 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -1,5 +1,6 @@ import { mapState } from 'pinia' import { computed } from 'vue' +import { mapGetters } from 'vuex' import ExtraNotifications from 'src/components/extra_notifications/extra_notifications.vue' import Notification from 'src/components/notification/notification.vue' @@ -15,7 +16,6 @@ import notificationsFetcher from '../../services/notifications_fetcher/notificat import NotificationFilters from './notification_filters.vue' import { useAnnouncementsStore } from 'src/stores/announcements.js' -import { useChatsStore } from 'src/stores/chats.js' import { useInterfaceStore } from 'src/stores/interface.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' @@ -115,14 +115,13 @@ const Notifications = { return countExtraNotifications( this.$store, useMergedConfigStore().mergedConfig, - useChatsStore().unreadChatsCount, useAnnouncementsStore().unreadAnnouncementCount, ) }, unseenCountTitle() { return ( this.unseenNotifications.length + - this.unreadChatsCount + + this.unreadChatCount + this.unreadAnnouncementCount ) }, @@ -161,7 +160,7 @@ const Notifications = { return !this.noExtra }, ...mapState(useAnnouncementsStore, ['unreadAnnouncementCount']), - ...mapState(useChatsStore, ['unreadChatsCount']), + ...mapGetters(['unreadChatCount']), }, mounted() { this.scrollerRef = this.$refs.root.closest('.column.-scrollable') diff --git a/src/components/poll/poll_form.js b/src/components/poll/poll_form.js index 22311d348..e89935c7e 100644 --- a/src/components/poll/poll_form.js +++ b/src/components/poll/poll_form.js @@ -16,45 +16,42 @@ export default { }, name: 'PollForm', props: { - visible: Boolean, - modelValue: { + visible: {}, + params: { type: Object, - required: false, - default: null, + required: true, }, }, - emits: ['update:modelValue'], computed: { pollType: { get() { - return pollFallback(this.modelValue, 'pollType') + return pollFallback(this.params, 'pollType') }, set(newVal) { - this.$emit('update:modelValue', { ...this.modelValue, pollType: newVal }) + this.params.pollType = newVal }, }, - options: { - get() { - return pollFallback(this.modelValue, 'options') - }, - set(newVal) { - this.$emit('update:modelValue', { ...this.modelValue, options: newVal }) + options() { + const hasOptions = !!this.params.options + if (!hasOptions) { + this.params.options = pollFallback(this.params, 'options') } + return this.params.options }, expiryAmount: { get() { - return pollFallback(this.modelValue, 'expiryAmount') + return pollFallback(this.params, 'expiryAmount') }, set(newVal) { - this.$emit('update:modelValue', { ...this.modelValue, expiryAmount: newVal }) + this.params.expiryAmount = newVal }, }, expiryUnit: { get() { - return pollFallback(this.modelValue, 'expiryUnit') + return pollFallback(this.params, 'expiryUnit') }, set(newVal) { - this.$emit('update:modelValue', { ...this.modelValue, expiryUnit: newVal }) + this.params.expiryUnit = newVal }, }, pollLimits() { @@ -91,6 +88,12 @@ export default { }, }, methods: { + clear() { + this.pollType = 'single' + this.options = ['', ''] + this.expiryAmount = 10 + this.expiryUnit = 'minutes' + }, nextOption(index) { const element = this.$el.querySelector(`#poll-${index + 1}`) if (element) { @@ -107,7 +110,7 @@ export default { }, addOption() { if (this.options.length < this.maxOptions) { - this.options = [...this.options, ''] + this.options.push('') return true } return false @@ -115,12 +118,8 @@ export default { deleteOption(index) { if (this.options.length > 2) { this.options.splice(index, 1) - this.options = this.options } }, - updateOption(index, value) { - this.options = this.options - }, convertExpiryToUnit(unit, amount) { // Note: we want seconds and not milliseconds return DateUtils.secondsToUnit(unit, amount) diff --git a/src/components/poll/poll_form.vue b/src/components/poll/poll_form.vue index cb8a6f91f..ea45fd17f 100644 --- a/src/components/poll/poll_form.vue +++ b/src/components/poll/poll_form.vue @@ -18,7 +18,6 @@ :placeholder="$t('polls.option')" :maxlength="maxLength" @keydown.enter.stop.prevent="nextOption(index)" - @change="updateOption" >
-
store.instanceIdentity.name, hideSitename: (store) => store.instanceIdentity.hideSitename, }), - ...mapState(useChatsStore, ['unreadChatsCount']), - ...mapGetters(['draftCount']), + ...mapGetters(['unreadChatCount', 'draftCount']), }, methods: { toggleDrawer() { diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index e0b7331c6..c034edf91 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -106,10 +106,10 @@ icon="comments" /> {{ $t("nav.chats") }} - {{ unreadChatsCount }} + {{ unreadChatCount }} diff --git a/src/components/status/status.scss b/src/components/status/status.scss index 1214d8b12..f710088d6 100644 --- a/src/components/status/status.scss +++ b/src/components/status/status.scss @@ -385,8 +385,4 @@ text-decoration: underline; } } - - .status-action-buttons { - margin-top: var(--status-margin); - } } diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 59cd6aaca..491f28e32 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -514,7 +514,6 @@ {{ button.counter?.(funcArg) }} diff --git a/src/components/status_action_buttons/action_button_container.js b/src/components/status_action_buttons/action_button_container.js index fa4a529c6..1f4cfc3f6 100644 --- a/src/components/status_action_buttons/action_button_container.js +++ b/src/components/status_action_buttons/action_button_container.js @@ -42,7 +42,7 @@ export default { ), ), }, - props: ['button', 'status', 'defaultButton', 'hideLabel'], + props: ['button', 'status'], emits: ['emojiPickerShown'], mounted() { if (this.button.name === 'mute') { diff --git a/src/components/status_action_buttons/action_button_container.vue b/src/components/status_action_buttons/action_button_container.vue index 72b0136df..24f9a3c3b 100644 --- a/src/components/status_action_buttons/action_button_container.vue +++ b/src/components/status_action_buttons/action_button_container.vue @@ -10,7 +10,6 @@ @@ -132,7 +131,6 @@ v-else :button="button" :status="status" - :hide-label="hideLabel" v-bind="$attrs" @emoji-picker-shown="e => $emit('emojiPickerShown', e)" /> diff --git a/src/components/status_action_buttons/status_action_buttons.js b/src/components/status_action_buttons/status_action_buttons.js index d9c7943b7..2341d21c5 100644 --- a/src/components/status_action_buttons/status_action_buttons.js +++ b/src/components/status_action_buttons/status_action_buttons.js @@ -15,31 +15,7 @@ import { faEllipsisH } from '@fortawesome/free-solid-svg-icons' library.add(faEllipsisH) const StatusActionButtons = { - props: { - status: { - type: Object, - required: true, - }, - replying: { - type: Boolean, - default: false, - }, - fixedPinned: { - type: Boolean, - default: false, - }, - pinned: { - type: Set, - }, - useDefaultButtons: { - type: Boolean, - default: false, - }, - hideLabels: { - type: Boolean, - default: false, - }, - }, + props: ['status', 'replying'], emits: ['toggleReplying', 'onSuccess', 'onError'], data() { return { @@ -60,20 +36,14 @@ const StatusActionButtons = { ConfirmModal: defineAsyncComponent( () => import('src/components/confirm_modal/confirm_modal.vue'), ), + ActionButtonContainer, }, computed: { ...mapState(useSyncConfigStore, { - userPinnedItems: (store) => + pinnedItems: (store) => new Set(store.prefsStorage.collections.pinnedStatusActions), }), - pinnedItems() { - if (this.fixedPinned) { - return this.pinned - } else { - return this.userPinnedItems - } - }, buttons() { return BUTTONS.filter((x) => (x.if ? x.if(this.funcArg) : true)) }, diff --git a/src/components/status_action_buttons/status_action_buttons.scss b/src/components/status_action_buttons/status_action_buttons.scss index b37343203..db149b418 100644 --- a/src/components/status_action_buttons/status_action_buttons.scss +++ b/src/components/status_action_buttons/status_action_buttons.scss @@ -8,6 +8,7 @@ grid-auto-flow: row dense; grid-auto-rows: 1fr; grid-gap: 0.5em 0.1em; + margin-top: var(--status-margin); } .pin-action-button { @@ -16,12 +17,6 @@ padding: 0.5em; margin: 0; } - - .quick-action.popover-wrapper { - button { - padding: 0 - } - } } // popover .extra-action-buttons { diff --git a/src/components/status_action_buttons/status_action_buttons.vue b/src/components/status_action_buttons/status_action_buttons.vue index 34e5e25ea..e0432048a 100644 --- a/src/components/status_action_buttons/status_action_buttons.vue +++ b/src/components/status_action_buttons/status_action_buttons.vue @@ -21,8 +21,6 @@ :close="() => { /* no-op */ }" :do-action="doAction" @emoji-picker-shown="onEmojiPickerShown" - :default-button-style="useDefaultButtons" - :hide-label="hideLabels" />