diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1257cd75f..056a0881b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,21 +6,28 @@ 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
+- 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
-
-### Added
-- Import/export a muted users
-- Proper handling of deletes when using websocket streaming
+- 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/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/chat.js b/src/components/chat/chat.js
index c0c9ad6cf..e57fcb91e 100644
--- a/src/components/chat/chat.js
+++ b/src/components/chat/chat.js
@@ -6,12 +6,13 @@ 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,
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)
@@ -281,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 })
+ }
})
})
})
@@ -309,42 +323,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.scss b/src/components/chat/chat.scss
index 787514c8b..aef58495c 100644
--- a/src/components/chat/chat.scss
+++ b/src/components/chat/chat.scss
@@ -138,11 +138,21 @@
}
.chat-view-heading {
+ box-sizing: border-box;
position: static;
z-index: 9999;
top: 0;
margin-top: 0;
border-radius: 0;
+
+ /* 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;
}
.scrollable-message-list {
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/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
+}
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 @@
/>
+
{{ unreadChatCount }}
@@ -47,7 +47,7 @@
/>{{ $t("nav.friend_requests") }}
{{ followRequestCount }}
@@ -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/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 1bdf9833b..de583269d 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -75,7 +75,8 @@ const PostStatusForm = {
'autoFocus',
'fileLimit',
'submitOnEnter',
- 'emojiPickerPlacement'
+ 'emojiPickerPlacement',
+ 'optimisticPosting'
],
components: {
MediaUpload,
@@ -272,7 +273,7 @@ const PostStatusForm = {
if (this.preview) this.previewStatus()
},
async postStatus (event, newStatus, opts = {}) {
- if (this.posting) { return }
+ if (this.posting && !this.optimisticPosting) { return }
if (this.disableSubmit) { return }
if (this.emojiInputShown) { return }
if (this.submitOnEnter) {
@@ -280,6 +281,8 @@ const PostStatusForm = {
event.preventDefault()
}
+ if (this.optimisticPosting && (this.emptyStatus || this.isOverLengthLimit)) { return }
+
if (this.emptyStatus) {
this.error = this.$t('post_status.empty_status_error')
return
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 2b57ea195..42d3152b5 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -129,7 +129,7 @@
v-model="newStatus.spoilerText"
type="text"
:placeholder="$t('post_status.content_warning')"
- :disabled="posting"
+ :disabled="posting && !optimisticPosting"
size="1"
class="form-post-subject"
>
@@ -155,7 +155,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/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index ed1ccb7db..28c888fe0 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") }}
{{ followRequestCount }}
@@ -272,12 +272,11 @@
--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);
+ .badge {
+ position: absolute;
+ right: 0.7rem;
+ top: 1em;
}
-
}
.side-drawer-logo-wrapper {
diff --git a/src/components/status/status.scss b/src/components/status/status.scss
index 0a395086c..0a94de32c 100644
--- a/src/components/status/status.scss
+++ b/src/components/status/status.scss
@@ -59,6 +59,15 @@ $status-margin: 0.75em;
justify-content: flex-end;
}
+ ._misclick-prevention & {
+ pointer-events: none;
+
+ .attachments {
+ pointer-events: initial;
+ cursor: initial;
+ }
+ }
+
.left-side {
margin-right: $status-margin;
}
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 368ee81be..cba46daf6 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -72,7 +72,7 @@ const Timeline = {
},
classes () {
let rootClasses = !this.embedded ? ['panel', 'panel-default'] : []
- if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked'])
+ if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention'])
return {
root: rootClasses,
header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading'] : []),
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 373b946f2..04859852b 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -113,7 +113,7 @@
}
&.-blocked {
- pointer-events: none;
+ cursor: progress;
}
}
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/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/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..1fc4e390f 100644
--- a/src/services/chat_service/chat_service.js
+++ b/src/services/chat_service/chat_service.js
@@ -3,9 +3,10 @@ import _ from 'lodash'
const empty = (chatId) => {
return {
idIndex: {},
+ idempotencyKeyIndex: {},
messages: [],
newMessageCount: 0,
- lastSeenTimestamp: 0,
+ lastSeenMessageId: '0',
chatId: chatId,
minId: undefined,
maxId: undefined
@@ -13,10 +14,20 @@ 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.lastSeenMessageId = '0'
storage.minId = undefined
storage.maxId = 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,25 @@ 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) {
+ // 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
+ delete storage.idIndex[message.fakeId]
+
+ return
+ }
+ }
+
+ if (!storage.minId || (!message.pending && message.id < storage.minId)) {
storage.minId = message.id
}
@@ -55,20 +103,26 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => {
}
}
- if (!storage.idIndex[message.id]) {
- if (storage.lastSeenTimestamp < message.created_at) {
+ if (!storage.idIndex[message.id] && !isConfirmation(storage, message)) {
+ if (storage.lastSeenMessageId < message.id) {
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
- 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
@@ -76,7 +130,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 +202,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/static/logo.png b/static/logo.png
deleted file mode 100644
index 7744b1acc..000000000
Binary files a/static/logo.png and /dev/null differ
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..0251cae78 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'))
}
@@ -44,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)
})
})