From e798e9a4177f025dda2b40d109fa40c2ebfd814e Mon Sep 17 00:00:00 2001 From: eugenijm Date: Thu, 29 Oct 2020 13:33:06 +0300 Subject: [PATCH 01/15] Optimistic message sending for chat --- src/components/chat/chat.js | 76 ++++++++++++++----- src/components/chat/chat.vue | 1 + src/components/chat_message/chat_message.scss | 13 ++++ src/components/chat_message/chat_message.vue | 2 +- .../post_status_form/post_status_form.js | 7 +- .../post_status_form/post_status_form.vue | 4 +- src/modules/api.js | 18 +++-- src/modules/chats.js | 22 +++++- src/services/api/api.service.js | 17 ++++- src/services/chat_service/chat_service.js | 63 +++++++++++++-- src/services/chat_utils/chat_utils.js | 21 +++++ .../entity_normalizer.service.js | 3 + .../chat_service/chat_service.spec.js | 3 + 13 files changed, 206 insertions(+), 44 deletions(-) diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js index c0c9ad6cf..2887afb53 100644 --- a/src/components/chat/chat.js +++ b/src/components/chat/chat.js @@ -12,6 +12,7 @@ import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons' +import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js' library.add( faChevronDown, @@ -22,6 +23,7 @@ const BOTTOMED_OUT_OFFSET = 10 const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150 const SAFE_RESIZE_TIME_OFFSET = 100 const MARK_AS_READ_DELAY = 1500 +const MAX_RETRIES = 10 const Chat = { components: { @@ -35,7 +37,8 @@ const Chat = { hoveredMessageChainId: undefined, lastScrollPosition: {}, scrollableContainerHeight: '100%', - errorLoadingChat: false + errorLoadingChat: false, + messageRetriers: {} } }, created () { @@ -219,7 +222,10 @@ const Chat = { if (!(this.currentChatMessageService && this.currentChatMessageService.maxId)) { return } if (document.hidden) { return } const lastReadId = this.currentChatMessageService.maxId - this.$store.dispatch('readChat', { id: this.currentChat.id, lastReadId }) + this.$store.dispatch('readChat', { + id: this.currentChat.id, + lastReadId + }) }, bottomedOut (offset) { return isBottomedOut(this.$refs.scrollable, offset) @@ -309,42 +315,74 @@ const Chat = { }) this.fetchChat({ isFirstFetch: true }) }, - sendMessage ({ status, media }) { + 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. + setTimeout(() => { + this.updateScrollableContainerHeight() + }, SAFE_RESIZE_TIME_OFFSET) + this.scrollDown({ forceRead: true }) + }) + }, + sendMessage ({ status, media, idempotencyKey }) { const params = { id: this.currentChat.id, - content: status + content: status, + idempotencyKey } if (media[0]) { params.mediaId = media[0].id } - return this.backendInteractor.sendChatMessage(params) + const fakeMessage = buildFakeMessage({ + attachments: media, + chatId: this.currentChat.id, + content: status, + userId: this.currentUser.id, + idempotencyKey + }) + + this.$store.dispatch('addChatMessages', { + chatId: this.currentChat.id, + messages: [fakeMessage] + }).then(() => { + this.handleAttachmentPosting() + }) + + return this.doSendMessage({ params, fakeMessage, retriesLeft: MAX_RETRIES }) + }, + doSendMessage ({ params, fakeMessage, retriesLeft = MAX_RETRIES }) { + if (retriesLeft <= 0) return + + this.backendInteractor.sendChatMessage(params) .then(data => { this.$store.dispatch('addChatMessages', { chatId: this.currentChat.id, - messages: [data], - updateMaxId: false - }).then(() => { - 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. - setTimeout(() => { - this.updateScrollableContainerHeight() - }, SAFE_RESIZE_TIME_OFFSET) - this.scrollDown({ forceRead: true }) - }) + updateMaxId: false, + messages: [{ ...data, fakeId: fakeMessage.id }] }) return data }) .catch(error => { console.error('Error sending message', error) - return { - error: this.$t('chats.error_sending_message') + 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 {} }) + + return Promise.resolve(fakeMessage) }, goBack () { this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } }) diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue index 5f58b9a63..94a0097c1 100644 --- a/src/components/chat/chat.vue +++ b/src/components/chat/chat.vue @@ -80,6 +80,7 @@ :disable-sensitivity-checkbox="true" :disable-submit="errorLoadingChat || !currentChat" :disable-preview="true" + :optimistic-posting="true" :post-handler="sendMessage" :submit-on-enter="!mobileLayout" :preserve-focus="!mobileLayout" diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss index 53ca7ccec..5af744a35 100644 --- a/src/components/chat_message/chat_message.scss +++ b/src/components/chat_message/chat_message.scss @@ -101,6 +101,19 @@ } } + .pending { + .status-content.media-body, .created-at { + color: var(--faint); + } + } + + .error { + .status-content.media-body, .created-at { + color: $fallback--cRed; + color: var(--badgeNotification, $fallback--cRed); + } + } + .incoming { a { color: var(--chatMessageIncomingLink, $fallback--link); diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue index d5b8bb9e5..3849ab6e7 100644 --- a/src/components/chat_message/chat_message.vue +++ b/src/components/chat_message/chat_message.vue @@ -32,7 +32,7 @@ >
@@ -150,7 +150,7 @@ :placeholder="placeholder || $t('post_status.default')" rows="1" cols="1" - :disabled="posting" + :disabled="posting && !optimisticPosting" class="form-post-body" :class="{ 'scrollable-form': !!maxHeight }" @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)" diff --git a/src/modules/api.js b/src/modules/api.js index 0a354c3f3..08485a30a 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -75,12 +75,18 @@ const api = { } else if (message.event === 'delete') { dispatch('deleteStatusById', message.id) } else if (message.event === 'pleroma:chat_update') { - dispatch('addChatMessages', { - chatId: message.chatUpdate.id, - messages: [message.chatUpdate.lastMessage] - }) - dispatch('updateChat', { chat: message.chatUpdate }) - maybeShowChatNotification(store, message.chatUpdate) + // The setTimeout wrapper is a temporary band-aid to avoid duplicates for the user's own messages when doing optimistic sending. + // The cause of the duplicates is the WS event arriving earlier than the HTTP response. + // This setTimeout wrapper can be removed once the commit `8e41baff` is in the stable Pleroma release. + // (`8e41baff` adds the idempotency key to the chat message entity, which PleromaFE uses when it's available, and it makes this artificial delay unnecessary). + setTimeout(() => { + dispatch('addChatMessages', { + chatId: message.chatUpdate.id, + messages: [message.chatUpdate.lastMessage] + }) + dispatch('updateChat', { chat: message.chatUpdate }) + maybeShowChatNotification(store, message.chatUpdate) + }, 100) } } ) diff --git a/src/modules/chats.js b/src/modules/chats.js index 21e30933c..0a373d88a 100644 --- a/src/modules/chats.js +++ b/src/modules/chats.js @@ -16,7 +16,8 @@ const defaultState = { openedChats: {}, openedChatMessageServices: {}, fetcher: undefined, - currentChatId: null + currentChatId: null, + lastReadMessageId: null } const getChatById = (state, id) => { @@ -92,9 +93,14 @@ const chats = { commit('setCurrentChatFetcher', { fetcher: undefined }) }, readChat ({ rootState, commit, dispatch }, { id, lastReadId }) { + const isNewMessage = rootState.chats.lastReadMessageId !== lastReadId + dispatch('resetChatNewMessageCount') - commit('readChat', { id }) - rootState.api.backendInteractor.readChat({ id, lastReadId }) + commit('readChat', { id, lastReadId }) + + if (isNewMessage) { + rootState.api.backendInteractor.readChat({ id, lastReadId }) + } }, deleteChatMessage ({ rootState, commit }, value) { rootState.api.backendInteractor.deleteChatMessage(value) @@ -106,6 +112,9 @@ const chats = { }, clearOpenedChats ({ rootState, commit, dispatch, rootGetters }) { commit('clearOpenedChats', { commit }) + }, + handleMessageError ({ commit }, value) { + commit('handleMessageError', { commit, ...value }) } }, mutations: { @@ -208,11 +217,16 @@ const chats = { } } }, - readChat (state, { id }) { + readChat (state, { id, lastReadId }) { + state.lastReadMessageId = lastReadId const chat = getChatById(state, id) if (chat) { chat.unread = 0 } + }, + handleMessageError (state, { chatId, fakeId, isRetry }) { + const chatMessageService = state.openedChatMessageServices[chatId] + chatService.handleMessageError(chatMessageService, fakeId, isRetry) } } } diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 1a3495d41..22b5e8bae 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -129,7 +129,11 @@ const promisedRequest = ({ method, url, params, payload, credentials, headers = return reject(new StatusCodeError(response.status, json, { url, options }, response)) } return resolve(json) - })) + }) + .catch((error) => { + return reject(new StatusCodeError(response.status, error, { url, options }, response)) + }) + ) }) } @@ -1210,7 +1214,7 @@ const chatMessages = ({ id, credentials, maxId, sinceId, limit = 20 }) => { }) } -const sendChatMessage = ({ id, content, mediaId = null, credentials }) => { +const sendChatMessage = ({ id, content, mediaId = null, idempotencyKey, credentials }) => { const payload = { 'content': content } @@ -1219,11 +1223,18 @@ const sendChatMessage = ({ id, content, mediaId = null, credentials }) => { payload['media_id'] = mediaId } + const headers = {} + + if (idempotencyKey) { + headers['idempotency-key'] = idempotencyKey + } + return promisedRequest({ url: PLEROMA_CHAT_MESSAGES_URL(id), method: 'POST', payload: payload, - credentials + credentials, + headers }) } diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js index 95c694823..815af82e6 100644 --- a/src/services/chat_service/chat_service.js +++ b/src/services/chat_service/chat_service.js @@ -3,6 +3,7 @@ import _ from 'lodash' const empty = (chatId) => { return { idIndex: {}, + idempotencyKeyIndex: {}, messages: [], newMessageCount: 0, lastSeenTimestamp: 0, @@ -13,8 +14,18 @@ const empty = (chatId) => { } const clear = (storage) => { - storage.idIndex = {} - storage.messages.splice(0, storage.messages.length) + const failedMessageIds = [] + + for (const message of storage.messages) { + if (message.error) { + failedMessageIds.push(message.id) + } else { + delete storage.idIndex[message.id] + delete storage.idempotencyKeyIndex[message.id] + } + } + + storage.messages = storage.messages.filter(m => failedMessageIds.includes(m.id)) storage.newMessageCount = 0 storage.lastSeenTimestamp = 0 storage.minId = undefined @@ -37,6 +48,25 @@ const deleteMessage = (storage, messageId) => { } } +const handleMessageError = (storage, fakeId, isRetry) => { + if (!storage) { return } + const fakeMessage = storage.idIndex[fakeId] + if (fakeMessage) { + fakeMessage.error = true + fakeMessage.pending = false + if (!isRetry) { + // Ensure the failed message doesn't stay at the bottom of the list. + const lastPersistedMessage = _.orderBy(storage.messages, ['pending', 'id'], ['asc', 'desc'])[0] + if (lastPersistedMessage) { + const oldId = fakeMessage.id + fakeMessage.id = `${lastPersistedMessage.id}-${new Date().getTime()}` + storage.idIndex[fakeMessage.id] = fakeMessage + delete storage.idIndex[oldId] + } + } + } +} + const add = (storage, { messages: newMessages, updateMaxId = true }) => { if (!storage) { return } for (let i = 0; i < newMessages.length; i++) { @@ -45,7 +75,19 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => { // sanity check if (message.chat_id !== storage.chatId) { return } - if (!storage.minId || message.id < storage.minId) { + if (message.fakeId) { + const fakeMessage = storage.idIndex[message.fakeId] + if (fakeMessage) { + Object.assign(fakeMessage, message, { error: false }) + delete fakeMessage['fakeId'] + storage.idIndex[fakeMessage.id] = fakeMessage + delete storage.idIndex[message.fakeId] + + return + } + } + + if (!storage.minId || (!message.pending && message.id < storage.minId)) { storage.minId = message.id } @@ -55,16 +97,22 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => { } } - if (!storage.idIndex[message.id]) { + if (!storage.idIndex[message.id] && !isConfirmation(storage, message)) { if (storage.lastSeenTimestamp < message.created_at) { storage.newMessageCount++ } - storage.messages.push(message) storage.idIndex[message.id] = message + storage.messages.push(storage.idIndex[message.id]) + storage.idempotencyKeyIndex[message.idempotency_key] = true } } } +const isConfirmation = (storage, message) => { + if (!message.idempotency_key) return + return storage.idempotencyKeyIndex[message.idempotency_key] +} + const resetNewMessageCount = (storage) => { if (!storage) { return } storage.newMessageCount = 0 @@ -76,7 +124,7 @@ const getView = (storage) => { if (!storage) { return [] } const result = [] - const messages = _.sortBy(storage.messages, ['id', 'desc']) + const messages = _.orderBy(storage.messages, ['pending', 'id'], ['asc', 'asc']) const firstMessage = messages[0] let previousMessage = messages[messages.length - 1] let currentMessageChainId @@ -148,7 +196,8 @@ const ChatService = { getView, deleteMessage, resetNewMessageCount, - clear + clear, + handleMessageError } export default ChatService diff --git a/src/services/chat_utils/chat_utils.js b/src/services/chat_utils/chat_utils.js index 86fe1af96..de6e0625d 100644 --- a/src/services/chat_utils/chat_utils.js +++ b/src/services/chat_utils/chat_utils.js @@ -18,3 +18,24 @@ export const maybeShowChatNotification = (store, chat) => { showDesktopNotification(store.rootState, opts) } + +export const buildFakeMessage = ({ content, chatId, attachments, userId, idempotencyKey }) => { + const fakeMessage = { + content, + chat_id: chatId, + created_at: new Date(), + id: `${new Date().getTime()}`, + attachments: attachments, + account_id: userId, + idempotency_key: idempotencyKey, + emojis: [], + pending: true, + isNormalized: true + } + + if (attachments[0]) { + fakeMessage.attachment = attachments[0] + } + + return fakeMessage +} diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 1884478a6..9d09b8d04 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -429,6 +429,9 @@ export const parseChatMessage = (message) => { } else { output.attachments = [] } + output.pending = !!message.pending + output.error = false + output.idempotency_key = message.idempotency_key output.isNormalized = true return output } diff --git a/test/unit/specs/services/chat_service/chat_service.spec.js b/test/unit/specs/services/chat_service/chat_service.spec.js index 2eb89a2df..15e64bb58 100644 --- a/test/unit/specs/services/chat_service/chat_service.spec.js +++ b/test/unit/specs/services/chat_service/chat_service.spec.js @@ -2,17 +2,20 @@ import chatService from '../../../../../src/services/chat_service/chat_service.j const message1 = { id: '9wLkdcmQXD21Oy8lEX', + idempotency_key: '1', created_at: (new Date('2020-06-22T18:45:53.000Z')) } const message2 = { id: '9wLkdp6ihaOVdNj8Wu', + idempotency_key: '2', account_id: '9vmRb29zLQReckr5ay', created_at: (new Date('2020-06-22T18:45:56.000Z')) } const message3 = { id: '9wLke9zL4Dy4OZR2RM', + idempotency_key: '3', account_id: '9vmRb29zLQReckr5ay', created_at: (new Date('2020-07-22T18:45:59.000Z')) } From 78e5a639228ba846e38ae722590aec1310275c79 Mon Sep 17 00:00:00 2001 From: Eugenij Date: Sun, 1 Nov 2020 14:25:02 +0000 Subject: [PATCH 02/15] Apply 1 suggestion(s) to 1 file(s) --- src/services/chat_service/chat_service.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js index 815af82e6..b0905dc12 100644 --- a/src/services/chat_service/chat_service.js +++ b/src/services/chat_service/chat_service.js @@ -78,6 +78,12 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => { if (message.fakeId) { const fakeMessage = storage.idIndex[message.fakeId] if (fakeMessage) { + // In case the same id exists (chat update before POST response) + // make sure to remove the older duplicate message. + if (storage.idIndex[message.id]) { + delete storage.idIndex[message.id] + storage.messages = storage.messages.filter(msg => msg.id !== message.id) + } Object.assign(fakeMessage, message, { error: false }) delete fakeMessage['fakeId'] storage.idIndex[fakeMessage.id] = fakeMessage From 5e8db7ed938021854cec8864462879f29d159eea Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson Date: Mon, 2 Nov 2020 09:15:13 +0200 Subject: [PATCH 03/15] move from using timestamps to ids when tracking last seen in chats --- src/services/chat_service/chat_service.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js index b0905dc12..1fc4e390f 100644 --- a/src/services/chat_service/chat_service.js +++ b/src/services/chat_service/chat_service.js @@ -6,7 +6,7 @@ const empty = (chatId) => { idempotencyKeyIndex: {}, messages: [], newMessageCount: 0, - lastSeenTimestamp: 0, + lastSeenMessageId: '0', chatId: chatId, minId: undefined, maxId: undefined @@ -27,7 +27,7 @@ const clear = (storage) => { storage.messages = storage.messages.filter(m => failedMessageIds.includes(m.id)) storage.newMessageCount = 0 - storage.lastSeenTimestamp = 0 + storage.lastSeenMessageId = '0' storage.minId = undefined storage.maxId = undefined } @@ -104,7 +104,7 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => { } if (!storage.idIndex[message.id] && !isConfirmation(storage, message)) { - if (storage.lastSeenTimestamp < message.created_at) { + if (storage.lastSeenMessageId < message.id) { storage.newMessageCount++ } storage.idIndex[message.id] = message @@ -122,7 +122,7 @@ const isConfirmation = (storage, message) => { const resetNewMessageCount = (storage) => { if (!storage) { return } storage.newMessageCount = 0 - storage.lastSeenTimestamp = new Date() + storage.lastSeenMessageId = storage.maxId } // Inserts date separators and marks the head and tail if it's the chain of messages made by the same user From 757706425a1c865a0e64e9c530b3aa1e7d9e2494 Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson Date: Mon, 2 Nov 2020 09:42:09 +0200 Subject: [PATCH 04/15] fix test --- test/unit/specs/services/chat_service/chat_service.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/specs/services/chat_service/chat_service.spec.js b/test/unit/specs/services/chat_service/chat_service.spec.js index 15e64bb58..0251cae78 100644 --- a/test/unit/specs/services/chat_service/chat_service.spec.js +++ b/test/unit/specs/services/chat_service/chat_service.spec.js @@ -47,10 +47,10 @@ describe('chatService', () => { chatService.resetNewMessageCount(chat) expect(chat.newMessageCount).to.eql(0) + expect(chat.lastSeenMessageId).to.eql(message2.id) - const createdAt = new Date() - createdAt.setSeconds(createdAt.getSeconds() + 10) - chatService.add(chat, { messages: [ { message3, created_at: createdAt } ] }) + // Add message with higher id + chatService.add(chat, { messages: [ message3 ] }) expect(chat.newMessageCount).to.eql(1) }) }) From 279205ade562f2231da13dadc70a7cd1a071ca72 Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson Date: Mon, 2 Nov 2020 10:42:38 +0200 Subject: [PATCH 05/15] update changelog for optimistic chat posting --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1257cd75f..91996216e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Import/export a muted users - Proper handling of deletes when using websocket streaming +- Added optimistic chat message sending, so you can start writing next message before the previous one has been sent ## [2.1.1] - 2020-09-08 ### Changed From a1e7852c10ecd2a99711f8e0330afcc93ef1439d Mon Sep 17 00:00:00 2001 From: HJ <30-hj@users.noreply.git.pleroma.social> Date: Mon, 2 Nov 2020 09:36:23 +0000 Subject: [PATCH 06/15] Update CHANGELOG.md --- CHANGELOG.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91996216e..68cab40c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,22 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Added - New option to optimize timeline rendering to make the site more responsive (enabled by default) +- New instance option `logoLeft` to move logo to the left side in desktop nav bar +- Import/export a muted users +- Proper handling of deletes when using websocket streaming +- Added optimistic chat message sending, so you can start writing next message before the previous one has been sent -## [Unreleased patch] ### Fixed - Fixed chats list not updating its order when new messages come in - Fixed chat messages sometimes getting lost when you receive a message at the same time - Fixed clicking NSFW hider through status popover - Fixed chat-view back button being hard to click - Fixed fresh chat notifications being cleared immediately while leaving the chat view and not having time to actually see the messages +- Fixed multiple regressions in CSS styles +- Fixed multiple issues with input fields when using CJK font as default +- Fixed search field in navbar infringing into logo in some cases ### Changed - Clicking immediately when timeline shifts is now blocked to prevent misclicks - -### Added -- Import/export a muted users -- Proper handling of deletes when using websocket streaming -- Added optimistic chat message sending, so you can start writing next message before the previous one has been sent +- Icons changed from fontello (FontAwesome 4 + others) to FontAwesome 5 due to problems with fontello. +- Some icons changed for better accessibility (lock, globe) +- Logo is now clickable ## [2.1.1] - 2020-09-08 ### Changed From 3db218dec1f0abc14b00569f605dc9cf8225d26f Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 2 Nov 2020 19:37:01 +0200 Subject: [PATCH 07/15] change logo to svg --- CHANGELOG.md | 1 + src/modules/instance.js | 2 +- static/config.json | 2 +- static/logo.png | Bin 1304 -> 0 bytes static/logo.svg | 71 +++++++++++++++++++++++++++++++++++ static/terms-of-service.html | 2 +- 6 files changed, 75 insertions(+), 3 deletions(-) delete mode 100644 static/logo.png create mode 100644 static/logo.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 68cab40c3..3c338de89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Icons changed from fontello (FontAwesome 4 + others) to FontAwesome 5 due to problems with fontello. - Some icons changed for better accessibility (lock, globe) - Logo is now clickable +- Changed default logo to SVG version ## [2.1.1] - 2020-09-08 ### Changed diff --git a/src/modules/instance.js b/src/modules/instance.js index 0c35f5d6d..5f7bf0ec6 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -27,7 +27,7 @@ const defaultState = { hideSitename: false, hideUserStats: false, loginMethod: 'password', - logo: '/static/logo.png', + logo: '/static/logo.svg', logoMargin: '.2em', logoMask: true, logoLeft: false, diff --git a/static/config.json b/static/config.json index 4b70aea58..f59e645ac 100644 --- a/static/config.json +++ b/static/config.json @@ -10,7 +10,7 @@ "hideSitename": false, "hideUserStats": false, "loginMethod": "password", - "logo": "/static/logo.png", + "logo": "/static/logo.svg", "logoMargin": ".1em", "logoMask": true, "logoLeft": false, diff --git a/static/logo.png b/static/logo.png deleted file mode 100644 index 7744b1acc313c5a146410cc5a56d99629022c637..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1304 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!EX7WqAsj$Z!;#Vf4nJ zu$zG}W8o(6ETEudiEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$Qb2>Idb&7< zRLpsMcYk+GsKoJy-_NK;GHVO+WTgrkn%&CUJL3$`#~G}SkdwYB5?_T#i+135d93Qpk@18S>>&My13c81F zc|PxI)c#q1%cn=rwRd5U;y;mOHrsdF>0edXufFZDFp;|Z>x9Oxb;r2spWW-|_|3KV zTG6s87W)?2eqS@C$V{napOaJk=ASinB^x95TrVw+va!F|apC%lOG~|vi*?(l3qFl_ zt^366*LVMi`z`AK{b@d(`rXr|W6radeQL93x1V=AV!XE8q2yq_8O-06q&P-12EG>MT9I`?JRw@c59u6-iG+dyE#LLC*QT!583ol!5J(j%I zoTvA~%eg^GtG~7_6tuDp(c2zpVB)mAJgYZ9Dj%$BY00}6+GPvAGW=Nn>28YAzinT; z78bfMPv5mYN?$^$yrgRJwI4V4MDt53ExgQb-{+S4 z`^w7a>$k}ng*Yu=n!5JmxrNT`^KC0XJ-NR2IM2-c7k}Dc4LY@c>osk`((O||Jp@{^ z@aqbmmUFVpE`JPOYrY{$?k;fTvwxe_{wcex<@)XOO8*IP&fRS( z8`!_yYR|PB-5d+oPjD|ME2_Q(Sl+2Rc!6W?y_KeN`Fj&HTKxF0Dz6m>1^>cL*TGV9 zt3F@jZgYF_<-!ZE*R0OUCBL5C`u8tCev6zTFiOI`I{C}W%EW-d4Ae7OI`;b9OxvW6 z7Qg;0*Oy&>slPUyXX1sI%~FB)X8?0g6u+dFe&mv^dxIWb{aEoS@U^0|T8Y%k@NC`p z`FV0R)z#I%zhBF4^LwGaZR^&p5jtY)P39DQuG0_jsx?*$` rEuQZ;$KvDPRqOc4% + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/static/terms-of-service.html b/static/terms-of-service.html index 3b6bbb36b..2b7bf7697 100644 --- a/static/terms-of-service.html +++ b/static/terms-of-service.html @@ -6,4 +6,4 @@ Pleroma install containing the real ToS for your instance.

See the Pleroma documentation for more information.


- + From 08e6e6664218d20cf3dd1b00823720505fb08fe7 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 2 Nov 2020 20:51:38 +0200 Subject: [PATCH 08/15] fix chat badge and unify styles across all badges including follow request count. --- src/App.scss | 21 ++++++++++------- .../chat_list_item/chat_list_item.vue | 12 +++++----- src/components/nav_panel/nav_panel.vue | 23 +++---------------- src/components/side_drawer/side_drawer.vue | 11 ++------- 4 files changed, 24 insertions(+), 43 deletions(-) diff --git a/src/App.scss b/src/App.scss index 1800d8163..ca7d33cd9 100644 --- a/src/App.scss +++ b/src/App.scss @@ -603,19 +603,24 @@ nav { flex-grow: 0; } } + .badge { + box-sizing: border-box; display: inline-block; border-radius: 99px; - min-width: 22px; - max-width: 22px; - min-height: 22px; - max-height: 22px; - font-size: 15px; - line-height: 22px; - text-align: center; + max-width: 10em; + min-width: 1.7em; + height: 1.3em; + padding: 0.15em 0.15em; vertical-align: middle; + font-weight: normal; + font-style: normal; + font-size: 0.9em; + line-height: 1; + text-align: center; white-space: nowrap; - padding: 0; + overflow: hidden; + text-overflow: ellipsis; &.badge-notification { background-color: $fallback--cRed; diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue index 1f8ecdf6a..cd3f436e2 100644 --- a/src/components/chat_list_item/chat_list_item.vue +++ b/src/components/chat_list_item/chat_list_item.vue @@ -21,6 +21,12 @@ /> +
+ +
-
- -
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 0f02a6815..0c83d0fe2 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -27,7 +27,7 @@
{{ unreadChatCount }}
@@ -47,7 +47,7 @@ />{{ $t("nav.friend_requests") }} @@ -84,12 +84,6 @@ padding: 0; } - .follow-request-count { - vertical-align: baseline; - background-color: $fallback--bg; - background-color: var(--input, $fallback--faint); - } - li { position: relative; border-bottom: 1px solid; @@ -156,21 +150,10 @@ margin-right: 0.8em; } - .unread-chat-count { - font-size: 0.9em; - font-weight: bolder; - font-style: normal; + .badge { position: absolute; right: 0.6rem; top: 1.25em; - padding: 0 0.3em; - min-width: 1.3rem; - min-height: 1.3rem; - max-height: 1.3rem; - line-height: 1.3rem; - max-width: 10em; - overflow: hidden; - text-overflow: ellipsis; } } diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index ed1ccb7db..149f11cb4 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -70,7 +70,7 @@ /> {{ $t("nav.chats") }} {{ unreadChatCount }} @@ -99,7 +99,7 @@ /> {{ $t("nav.friend_requests") }} @@ -271,13 +271,6 @@ --faintLink: var(--popoverFaintLink, $fallback--faint); --lightText: var(--popoverLightText, $fallback--lightText); --icon: var(--popoverIcon, $fallback--icon); - - .follow-request-count { - vertical-align: baseline; - background-color: $fallback--bg; - background-color: var(--input, $fallback--faint); - } - } .side-drawer-logo-wrapper { From bdf2f36f11a0768dd21c28ba149de4d5e4cc2ad5 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 2 Nov 2020 20:57:56 +0200 Subject: [PATCH 09/15] fix chat heading not being aligned and using wrong styles --- src/components/chat/chat.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss index 787514c8b..4abd94a17 100644 --- a/src/components/chat/chat.scss +++ b/src/components/chat/chat.scss @@ -138,11 +138,15 @@ } .chat-view-heading { + box-sizing: border-box; position: static; z-index: 9999; top: 0; margin-top: 0; border-radius: 0; + background: linear-gradient(to top, var(--panel), var(--panel)), + linear-gradient(to top, var(--bg), var(--bg)); + height: 50px; } .scrollable-message-list { From b6a8ca44ef84b6fb78958a2af3bffd467804a061 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 2 Nov 2020 21:08:22 +0200 Subject: [PATCH 10/15] added comment --- src/components/chat/chat.scss | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss index 4abd94a17..aef58495c 100644 --- a/src/components/chat/chat.scss +++ b/src/components/chat/chat.scss @@ -144,8 +144,14 @@ top: 0; margin-top: 0; border-radius: 0; - background: linear-gradient(to top, var(--panel), var(--panel)), - linear-gradient(to top, var(--bg), var(--bg)); + + /* This practically overlays the panel heading color over panel background + * color. This is needed because we allow transparent panel background and + * it doesn't work well in this "disjointed panel header" case + */ + background: + linear-gradient(to top, var(--panel), var(--panel)), + linear-gradient(to top, var(--bg), var(--bg)); height: 50px; } From e351665bb31745e2639abef3e47bf4f9df616d6b Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 2 Nov 2020 23:43:32 +0200 Subject: [PATCH 11/15] Instead of blocking all interaction, only block interaction in places that matter --- src/components/extra_buttons/extra_buttons.vue | 4 ++++ src/components/favorite_button/favorite_button.vue | 4 ++++ src/components/react_button/react_button.vue | 4 ++++ src/components/reply_button/reply_button.vue | 4 ++++ src/components/retweet_button/retweet_button.vue | 4 ++++ src/components/status/status.scss | 14 ++++++++++++++ src/components/status/status.vue | 1 + src/components/status_content/status_content.vue | 4 ++++ src/components/timeline/timeline.js | 2 +- src/components/timeline/timeline.vue | 2 +- src/components/user_card/user_card.vue | 4 ++++ 11 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue index a33f6e87a..984b04836 100644 --- a/src/components/extra_buttons/extra_buttons.vue +++ b/src/components/extra_buttons/extra_buttons.vue @@ -120,5 +120,9 @@ color: $fallback--text; color: var(--text, $fallback--text); } + + ._misclick-prevention & { + pointer-events: none !important; + } } diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue index dfe12f867..55872133a 100644 --- a/src/components/favorite_button/favorite_button.vue +++ b/src/components/favorite_button/favorite_button.vue @@ -35,6 +35,10 @@ color: $fallback--cOrange; color: var(--cOrange, $fallback--cOrange); } + + ._misclick-prevention & { + pointer-events: none !important; + } } &.-favorited { diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue index 95d95b11d..bb4472d71 100644 --- a/src/components/react_button/react_button.vue +++ b/src/components/react_button/react_button.vue @@ -107,6 +107,10 @@ color: $fallback--text; color: var(--text, $fallback--text); } + + ._misclick-prevention & { + pointer-events: none !important; + } } diff --git a/src/components/reply_button/reply_button.vue b/src/components/reply_button/reply_button.vue index a0ac89416..4ca37d5ed 100644 --- a/src/components/reply_button/reply_button.vue +++ b/src/components/reply_button/reply_button.vue @@ -34,6 +34,10 @@ color: $fallback--cBlue; color: var(--cBlue, $fallback--cBlue); } + + ._misclick-prevention & { + pointer-events: none !important; + } } } diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue index b234f3d91..b79fcd757 100644 --- a/src/components/retweet_button/retweet_button.vue +++ b/src/components/retweet_button/retweet_button.vue @@ -45,6 +45,10 @@ color: $fallback--cGreen; color: var(--cGreen, $fallback--cGreen); } + + ._misclick-prevention & { + pointer-events: none !important; + } } &.-repeated { diff --git a/src/components/status/status.scss b/src/components/status/status.scss index 0a395086c..769f7ef4b 100644 --- a/src/components/status/status.scss +++ b/src/components/status/status.scss @@ -59,6 +59,12 @@ $status-margin: 0.75em; justify-content: flex-end; } + .user-avatar { + ._misclick-prevention & { + pointer-events: none !important; + } + } + .left-side { margin-right: $status-margin; } @@ -108,6 +114,10 @@ $status-margin: 0.75em; a { display: inline-block; word-break: break-all; + + ._misclick-prevention & { + pointer-events: none !important; + } } } @@ -250,6 +260,10 @@ $status-margin: 0.75em; vertical-align: middle; object-fit: contain; } + + ._misclick-prevention & a { + pointer-events: none !important; + } } .status-fadein { diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 21412faa6..eb54befcb 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -119,6 +119,7 @@ > Date: Tue, 3 Nov 2020 11:35:55 +0200 Subject: [PATCH 12/15] fix mobile navbar hitboxes --- src/components/mobile_nav/mobile_nav.vue | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue index 6651fc8e3..5304a5006 100644 --- a/src/components/mobile_nav/mobile_nav.vue +++ b/src/components/mobile_nav/mobile_nav.vue @@ -110,12 +110,23 @@ } .mobile-nav-button { + display: inline-block; text-align: center; - margin: 0 1em; + padding: 0 1em; position: relative; cursor: pointer; } + .site-name { + padding: 0 .3em; + display: inline-block; + } + + .item { + /* moslty just to get rid of extra whitespaces */ + display: flex; + } + .alert-dot { border-radius: 100%; height: 8px; From d126eddfcac6e54f337e5aa3b9744a46f01e7ba9 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 3 Nov 2020 18:39:46 +0200 Subject: [PATCH 13/15] change approach to disable all, enable some --- src/components/extra_buttons/extra_buttons.vue | 4 ---- .../favorite_button/favorite_button.vue | 4 ---- src/components/react_button/react_button.vue | 4 ---- src/components/reply_button/reply_button.vue | 4 ---- .../retweet_button/retweet_button.vue | 4 ---- src/components/status/status.scss | 17 ++++++----------- src/components/status/status.vue | 1 - .../status_content/status_content.vue | 4 ---- src/components/user_card/user_card.vue | 4 ---- 9 files changed, 6 insertions(+), 40 deletions(-) diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue index 984b04836..a33f6e87a 100644 --- a/src/components/extra_buttons/extra_buttons.vue +++ b/src/components/extra_buttons/extra_buttons.vue @@ -120,9 +120,5 @@ color: $fallback--text; color: var(--text, $fallback--text); } - - ._misclick-prevention & { - pointer-events: none !important; - } } diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue index 55872133a..dfe12f867 100644 --- a/src/components/favorite_button/favorite_button.vue +++ b/src/components/favorite_button/favorite_button.vue @@ -35,10 +35,6 @@ color: $fallback--cOrange; color: var(--cOrange, $fallback--cOrange); } - - ._misclick-prevention & { - pointer-events: none !important; - } } &.-favorited { diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue index bb4472d71..95d95b11d 100644 --- a/src/components/react_button/react_button.vue +++ b/src/components/react_button/react_button.vue @@ -107,10 +107,6 @@ color: $fallback--text; color: var(--text, $fallback--text); } - - ._misclick-prevention & { - pointer-events: none !important; - } } diff --git a/src/components/reply_button/reply_button.vue b/src/components/reply_button/reply_button.vue index 4ca37d5ed..a0ac89416 100644 --- a/src/components/reply_button/reply_button.vue +++ b/src/components/reply_button/reply_button.vue @@ -34,10 +34,6 @@ color: $fallback--cBlue; color: var(--cBlue, $fallback--cBlue); } - - ._misclick-prevention & { - pointer-events: none !important; - } } } diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue index b79fcd757..b234f3d91 100644 --- a/src/components/retweet_button/retweet_button.vue +++ b/src/components/retweet_button/retweet_button.vue @@ -45,10 +45,6 @@ color: $fallback--cGreen; color: var(--cGreen, $fallback--cGreen); } - - ._misclick-prevention & { - pointer-events: none !important; - } } &.-repeated { diff --git a/src/components/status/status.scss b/src/components/status/status.scss index 769f7ef4b..0a94de32c 100644 --- a/src/components/status/status.scss +++ b/src/components/status/status.scss @@ -59,9 +59,12 @@ $status-margin: 0.75em; justify-content: flex-end; } - .user-avatar { - ._misclick-prevention & { - pointer-events: none !important; + ._misclick-prevention & { + pointer-events: none; + + .attachments { + pointer-events: initial; + cursor: initial; } } @@ -114,10 +117,6 @@ $status-margin: 0.75em; a { display: inline-block; word-break: break-all; - - ._misclick-prevention & { - pointer-events: none !important; - } } } @@ -260,10 +259,6 @@ $status-margin: 0.75em; vertical-align: middle; object-fit: contain; } - - ._misclick-prevention & a { - pointer-events: none !important; - } } .status-fadein { diff --git a/src/components/status/status.vue b/src/components/status/status.vue index eb54befcb..21412faa6 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -119,7 +119,6 @@ > Date: Tue, 3 Nov 2020 18:45:23 +0200 Subject: [PATCH 14/15] fix mobile badge alignment --- src/components/side_drawer/side_drawer.vue | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index 149f11cb4..28c888fe0 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -271,6 +271,12 @@ --faintLink: var(--popoverFaintLink, $fallback--faint); --lightText: var(--popoverLightText, $fallback--lightText); --icon: var(--popoverIcon, $fallback--icon); + + .badge { + position: absolute; + right: 0.7rem; + top: 1em; + } } .side-drawer-logo-wrapper { From 15ea9d8c917d6d0408a9c48f38976d19f9936054 Mon Sep 17 00:00:00 2001 From: eugenijm Date: Fri, 6 Nov 2020 01:20:08 +0300 Subject: [PATCH 15/15] Fix the chat scroll behavior for vertical screens. Fetch the messages until the scrollbar becomes visible, so that the user always has the ability to scroll up and load new messages. --- CHANGELOG.md | 1 + src/components/chat/chat.js | 10 +++++++++- src/components/chat/chat_layout_utils.js | 7 +++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c338de89..056a0881b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fixed multiple regressions in CSS styles - Fixed multiple issues with input fields when using CJK font as default - Fixed search field in navbar infringing into logo in some cases +- Fixed not being able to load the chat history in vertical screens when the message list doesn't take the full height of the scrollable container on the first fetch. ### Changed - Clicking immediately when timeline shifts is now blocked to prevent misclicks diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js index 2887afb53..e57fcb91e 100644 --- a/src/components/chat/chat.js +++ b/src/components/chat/chat.js @@ -6,7 +6,7 @@ import PostStatusForm from '../post_status_form/post_status_form.vue' import ChatTitle from '../chat_title/chat_title.vue' import chatService from '../../services/chat_service/chat_service.js' import { promiseInterval } from '../../services/promise_interval/promise_interval.js' -import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight } from './chat_layout_utils.js' +import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight, isScrollable } from './chat_layout_utils.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronDown, @@ -287,6 +287,14 @@ const Chat = { if (isFirstFetch) { this.updateScrollableContainerHeight() } + + // 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(this.$refs.scrollable) && messages.length > 0) { + this.fetchChat({ maxId: this.currentChatMessageService.minId }) + } }) }) }) diff --git a/src/components/chat/chat_layout_utils.js b/src/components/chat/chat_layout_utils.js index 609dc0c9b..50a933ac7 100644 --- a/src/components/chat/chat_layout_utils.js +++ b/src/components/chat/chat_layout_utils.js @@ -24,3 +24,10 @@ export const isBottomedOut = (el, offset = 0) => { export const scrollableContainerHeight = (inner, header, footer) => { return inner.offsetHeight - header.clientHeight - footer.clientHeight } + +// Returns whether or not the scrollbar is visible. +export const isScrollable = (el) => { + if (!el) return + + return el.scrollHeight > el.clientHeight +}