forked from AkkomaGang/akkoma-fe
Merge branch 'optimistic-chat-posting' into 'develop'
Optimistic / nonblocking message posting for chats See merge request pleroma/pleroma-fe!1228
This commit is contained in:
commit
5254fdba75
13 changed files with 212 additions and 44 deletions
|
@ -12,6 +12,7 @@ import {
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
faChevronLeft
|
faChevronLeft
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
|
@ -22,6 +23,7 @@ const BOTTOMED_OUT_OFFSET = 10
|
||||||
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150
|
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150
|
||||||
const SAFE_RESIZE_TIME_OFFSET = 100
|
const SAFE_RESIZE_TIME_OFFSET = 100
|
||||||
const MARK_AS_READ_DELAY = 1500
|
const MARK_AS_READ_DELAY = 1500
|
||||||
|
const MAX_RETRIES = 10
|
||||||
|
|
||||||
const Chat = {
|
const Chat = {
|
||||||
components: {
|
components: {
|
||||||
|
@ -35,7 +37,8 @@ const Chat = {
|
||||||
hoveredMessageChainId: undefined,
|
hoveredMessageChainId: undefined,
|
||||||
lastScrollPosition: {},
|
lastScrollPosition: {},
|
||||||
scrollableContainerHeight: '100%',
|
scrollableContainerHeight: '100%',
|
||||||
errorLoadingChat: false
|
errorLoadingChat: false,
|
||||||
|
messageRetriers: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
@ -219,7 +222,10 @@ const Chat = {
|
||||||
if (!(this.currentChatMessageService && this.currentChatMessageService.maxId)) { return }
|
if (!(this.currentChatMessageService && this.currentChatMessageService.maxId)) { return }
|
||||||
if (document.hidden) { return }
|
if (document.hidden) { return }
|
||||||
const lastReadId = this.currentChatMessageService.maxId
|
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) {
|
bottomedOut (offset) {
|
||||||
return isBottomedOut(this.$refs.scrollable, offset)
|
return isBottomedOut(this.$refs.scrollable, offset)
|
||||||
|
@ -309,42 +315,74 @@ const Chat = {
|
||||||
})
|
})
|
||||||
this.fetchChat({ isFirstFetch: true })
|
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 = {
|
const params = {
|
||||||
id: this.currentChat.id,
|
id: this.currentChat.id,
|
||||||
content: status
|
content: status,
|
||||||
|
idempotencyKey
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media[0]) {
|
if (media[0]) {
|
||||||
params.mediaId = media[0].id
|
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 => {
|
.then(data => {
|
||||||
this.$store.dispatch('addChatMessages', {
|
this.$store.dispatch('addChatMessages', {
|
||||||
chatId: this.currentChat.id,
|
chatId: this.currentChat.id,
|
||||||
messages: [data],
|
updateMaxId: false,
|
||||||
updateMaxId: false
|
messages: [{ ...data, fakeId: fakeMessage.id }]
|
||||||
}).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 })
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error sending message', error)
|
console.error('Error sending message', error)
|
||||||
return {
|
this.$store.dispatch('handleMessageError', {
|
||||||
error: this.$t('chats.error_sending_message')
|
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 () {
|
goBack () {
|
||||||
this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } })
|
this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } })
|
||||||
|
|
|
@ -80,6 +80,7 @@
|
||||||
:disable-sensitivity-checkbox="true"
|
:disable-sensitivity-checkbox="true"
|
||||||
:disable-submit="errorLoadingChat || !currentChat"
|
:disable-submit="errorLoadingChat || !currentChat"
|
||||||
:disable-preview="true"
|
:disable-preview="true"
|
||||||
|
:optimistic-posting="true"
|
||||||
:post-handler="sendMessage"
|
:post-handler="sendMessage"
|
||||||
:submit-on-enter="!mobileLayout"
|
:submit-on-enter="!mobileLayout"
|
||||||
:preserve-focus="!mobileLayout"
|
:preserve-focus="!mobileLayout"
|
||||||
|
|
|
@ -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 {
|
.incoming {
|
||||||
a {
|
a {
|
||||||
color: var(--chatMessageIncomingLink, $fallback--link);
|
color: var(--chatMessageIncomingLink, $fallback--link);
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="media status"
|
class="media status"
|
||||||
:class="{ 'without-attachment': !hasAttachment }"
|
:class="{ 'without-attachment': !hasAttachment, 'pending': chatViewItem.data.pending, 'error': chatViewItem.data.error }"
|
||||||
style="position: relative"
|
style="position: relative"
|
||||||
@mouseenter="hovered = true"
|
@mouseenter="hovered = true"
|
||||||
@mouseleave="hovered = false"
|
@mouseleave="hovered = false"
|
||||||
|
|
|
@ -75,7 +75,8 @@ const PostStatusForm = {
|
||||||
'autoFocus',
|
'autoFocus',
|
||||||
'fileLimit',
|
'fileLimit',
|
||||||
'submitOnEnter',
|
'submitOnEnter',
|
||||||
'emojiPickerPlacement'
|
'emojiPickerPlacement',
|
||||||
|
'optimisticPosting'
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
MediaUpload,
|
MediaUpload,
|
||||||
|
@ -272,7 +273,7 @@ const PostStatusForm = {
|
||||||
if (this.preview) this.previewStatus()
|
if (this.preview) this.previewStatus()
|
||||||
},
|
},
|
||||||
async postStatus (event, newStatus, opts = {}) {
|
async postStatus (event, newStatus, opts = {}) {
|
||||||
if (this.posting) { return }
|
if (this.posting && !this.optimisticPosting) { return }
|
||||||
if (this.disableSubmit) { return }
|
if (this.disableSubmit) { return }
|
||||||
if (this.emojiInputShown) { return }
|
if (this.emojiInputShown) { return }
|
||||||
if (this.submitOnEnter) {
|
if (this.submitOnEnter) {
|
||||||
|
@ -280,6 +281,8 @@ const PostStatusForm = {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.optimisticPosting && (this.emptyStatus || this.isOverLengthLimit)) { return }
|
||||||
|
|
||||||
if (this.emptyStatus) {
|
if (this.emptyStatus) {
|
||||||
this.error = this.$t('post_status.empty_status_error')
|
this.error = this.$t('post_status.empty_status_error')
|
||||||
return
|
return
|
||||||
|
|
|
@ -129,7 +129,7 @@
|
||||||
v-model="newStatus.spoilerText"
|
v-model="newStatus.spoilerText"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="$t('post_status.content_warning')"
|
:placeholder="$t('post_status.content_warning')"
|
||||||
:disabled="posting"
|
:disabled="posting && !optimisticPosting"
|
||||||
size="1"
|
size="1"
|
||||||
class="form-post-subject"
|
class="form-post-subject"
|
||||||
>
|
>
|
||||||
|
@ -155,7 +155,7 @@
|
||||||
:placeholder="placeholder || $t('post_status.default')"
|
:placeholder="placeholder || $t('post_status.default')"
|
||||||
rows="1"
|
rows="1"
|
||||||
cols="1"
|
cols="1"
|
||||||
:disabled="posting"
|
:disabled="posting && !optimisticPosting"
|
||||||
class="form-post-body"
|
class="form-post-body"
|
||||||
:class="{ 'scrollable-form': !!maxHeight }"
|
:class="{ 'scrollable-form': !!maxHeight }"
|
||||||
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
|
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
|
||||||
|
|
|
@ -75,12 +75,18 @@ const api = {
|
||||||
} else if (message.event === 'delete') {
|
} else if (message.event === 'delete') {
|
||||||
dispatch('deleteStatusById', message.id)
|
dispatch('deleteStatusById', message.id)
|
||||||
} else if (message.event === 'pleroma:chat_update') {
|
} else if (message.event === 'pleroma:chat_update') {
|
||||||
dispatch('addChatMessages', {
|
// The setTimeout wrapper is a temporary band-aid to avoid duplicates for the user's own messages when doing optimistic sending.
|
||||||
chatId: message.chatUpdate.id,
|
// The cause of the duplicates is the WS event arriving earlier than the HTTP response.
|
||||||
messages: [message.chatUpdate.lastMessage]
|
// 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).
|
||||||
dispatch('updateChat', { chat: message.chatUpdate })
|
setTimeout(() => {
|
||||||
maybeShowChatNotification(store, message.chatUpdate)
|
dispatch('addChatMessages', {
|
||||||
|
chatId: message.chatUpdate.id,
|
||||||
|
messages: [message.chatUpdate.lastMessage]
|
||||||
|
})
|
||||||
|
dispatch('updateChat', { chat: message.chatUpdate })
|
||||||
|
maybeShowChatNotification(store, message.chatUpdate)
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,7 +16,8 @@ const defaultState = {
|
||||||
openedChats: {},
|
openedChats: {},
|
||||||
openedChatMessageServices: {},
|
openedChatMessageServices: {},
|
||||||
fetcher: undefined,
|
fetcher: undefined,
|
||||||
currentChatId: null
|
currentChatId: null,
|
||||||
|
lastReadMessageId: null
|
||||||
}
|
}
|
||||||
|
|
||||||
const getChatById = (state, id) => {
|
const getChatById = (state, id) => {
|
||||||
|
@ -92,9 +93,14 @@ const chats = {
|
||||||
commit('setCurrentChatFetcher', { fetcher: undefined })
|
commit('setCurrentChatFetcher', { fetcher: undefined })
|
||||||
},
|
},
|
||||||
readChat ({ rootState, commit, dispatch }, { id, lastReadId }) {
|
readChat ({ rootState, commit, dispatch }, { id, lastReadId }) {
|
||||||
|
const isNewMessage = rootState.chats.lastReadMessageId !== lastReadId
|
||||||
|
|
||||||
dispatch('resetChatNewMessageCount')
|
dispatch('resetChatNewMessageCount')
|
||||||
commit('readChat', { id })
|
commit('readChat', { id, lastReadId })
|
||||||
rootState.api.backendInteractor.readChat({ id, lastReadId })
|
|
||||||
|
if (isNewMessage) {
|
||||||
|
rootState.api.backendInteractor.readChat({ id, lastReadId })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
deleteChatMessage ({ rootState, commit }, value) {
|
deleteChatMessage ({ rootState, commit }, value) {
|
||||||
rootState.api.backendInteractor.deleteChatMessage(value)
|
rootState.api.backendInteractor.deleteChatMessage(value)
|
||||||
|
@ -106,6 +112,9 @@ const chats = {
|
||||||
},
|
},
|
||||||
clearOpenedChats ({ rootState, commit, dispatch, rootGetters }) {
|
clearOpenedChats ({ rootState, commit, dispatch, rootGetters }) {
|
||||||
commit('clearOpenedChats', { commit })
|
commit('clearOpenedChats', { commit })
|
||||||
|
},
|
||||||
|
handleMessageError ({ commit }, value) {
|
||||||
|
commit('handleMessageError', { commit, ...value })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
|
@ -208,11 +217,16 @@ const chats = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
readChat (state, { id }) {
|
readChat (state, { id, lastReadId }) {
|
||||||
|
state.lastReadMessageId = lastReadId
|
||||||
const chat = getChatById(state, id)
|
const chat = getChatById(state, id)
|
||||||
if (chat) {
|
if (chat) {
|
||||||
chat.unread = 0
|
chat.unread = 0
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
handleMessageError (state, { chatId, fakeId, isRetry }) {
|
||||||
|
const chatMessageService = state.openedChatMessageServices[chatId]
|
||||||
|
chatService.handleMessageError(chatMessageService, fakeId, isRetry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,7 +129,11 @@ const promisedRequest = ({ method, url, params, payload, credentials, headers =
|
||||||
return reject(new StatusCodeError(response.status, json, { url, options }, response))
|
return reject(new StatusCodeError(response.status, json, { url, options }, response))
|
||||||
}
|
}
|
||||||
return resolve(json)
|
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 = {
|
const payload = {
|
||||||
'content': content
|
'content': content
|
||||||
}
|
}
|
||||||
|
@ -1219,11 +1223,18 @@ const sendChatMessage = ({ id, content, mediaId = null, credentials }) => {
|
||||||
payload['media_id'] = mediaId
|
payload['media_id'] = mediaId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const headers = {}
|
||||||
|
|
||||||
|
if (idempotencyKey) {
|
||||||
|
headers['idempotency-key'] = idempotencyKey
|
||||||
|
}
|
||||||
|
|
||||||
return promisedRequest({
|
return promisedRequest({
|
||||||
url: PLEROMA_CHAT_MESSAGES_URL(id),
|
url: PLEROMA_CHAT_MESSAGES_URL(id),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
payload: payload,
|
payload: payload,
|
||||||
credentials
|
credentials,
|
||||||
|
headers
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import _ from 'lodash'
|
||||||
const empty = (chatId) => {
|
const empty = (chatId) => {
|
||||||
return {
|
return {
|
||||||
idIndex: {},
|
idIndex: {},
|
||||||
|
idempotencyKeyIndex: {},
|
||||||
messages: [],
|
messages: [],
|
||||||
newMessageCount: 0,
|
newMessageCount: 0,
|
||||||
lastSeenTimestamp: 0,
|
lastSeenTimestamp: 0,
|
||||||
|
@ -13,8 +14,18 @@ const empty = (chatId) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const clear = (storage) => {
|
const clear = (storage) => {
|
||||||
storage.idIndex = {}
|
const failedMessageIds = []
|
||||||
storage.messages.splice(0, storage.messages.length)
|
|
||||||
|
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.newMessageCount = 0
|
||||||
storage.lastSeenTimestamp = 0
|
storage.lastSeenTimestamp = 0
|
||||||
storage.minId = undefined
|
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 }) => {
|
const add = (storage, { messages: newMessages, updateMaxId = true }) => {
|
||||||
if (!storage) { return }
|
if (!storage) { return }
|
||||||
for (let i = 0; i < newMessages.length; i++) {
|
for (let i = 0; i < newMessages.length; i++) {
|
||||||
|
@ -45,7 +75,25 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => {
|
||||||
// sanity check
|
// sanity check
|
||||||
if (message.chat_id !== storage.chatId) { return }
|
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
|
storage.minId = message.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,16 +103,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) {
|
if (storage.lastSeenTimestamp < message.created_at) {
|
||||||
storage.newMessageCount++
|
storage.newMessageCount++
|
||||||
}
|
}
|
||||||
storage.messages.push(message)
|
|
||||||
storage.idIndex[message.id] = 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) => {
|
const resetNewMessageCount = (storage) => {
|
||||||
if (!storage) { return }
|
if (!storage) { return }
|
||||||
storage.newMessageCount = 0
|
storage.newMessageCount = 0
|
||||||
|
@ -76,7 +130,7 @@ const getView = (storage) => {
|
||||||
if (!storage) { return [] }
|
if (!storage) { return [] }
|
||||||
|
|
||||||
const result = []
|
const result = []
|
||||||
const messages = _.sortBy(storage.messages, ['id', 'desc'])
|
const messages = _.orderBy(storage.messages, ['pending', 'id'], ['asc', 'asc'])
|
||||||
const firstMessage = messages[0]
|
const firstMessage = messages[0]
|
||||||
let previousMessage = messages[messages.length - 1]
|
let previousMessage = messages[messages.length - 1]
|
||||||
let currentMessageChainId
|
let currentMessageChainId
|
||||||
|
@ -148,7 +202,8 @@ const ChatService = {
|
||||||
getView,
|
getView,
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
resetNewMessageCount,
|
resetNewMessageCount,
|
||||||
clear
|
clear,
|
||||||
|
handleMessageError
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChatService
|
export default ChatService
|
||||||
|
|
|
@ -18,3 +18,24 @@ export const maybeShowChatNotification = (store, chat) => {
|
||||||
|
|
||||||
showDesktopNotification(store.rootState, opts)
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -429,6 +429,9 @@ export const parseChatMessage = (message) => {
|
||||||
} else {
|
} else {
|
||||||
output.attachments = []
|
output.attachments = []
|
||||||
}
|
}
|
||||||
|
output.pending = !!message.pending
|
||||||
|
output.error = false
|
||||||
|
output.idempotency_key = message.idempotency_key
|
||||||
output.isNormalized = true
|
output.isNormalized = true
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,17 +2,20 @@ import chatService from '../../../../../src/services/chat_service/chat_service.j
|
||||||
|
|
||||||
const message1 = {
|
const message1 = {
|
||||||
id: '9wLkdcmQXD21Oy8lEX',
|
id: '9wLkdcmQXD21Oy8lEX',
|
||||||
|
idempotency_key: '1',
|
||||||
created_at: (new Date('2020-06-22T18:45:53.000Z'))
|
created_at: (new Date('2020-06-22T18:45:53.000Z'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const message2 = {
|
const message2 = {
|
||||||
id: '9wLkdp6ihaOVdNj8Wu',
|
id: '9wLkdp6ihaOVdNj8Wu',
|
||||||
|
idempotency_key: '2',
|
||||||
account_id: '9vmRb29zLQReckr5ay',
|
account_id: '9vmRb29zLQReckr5ay',
|
||||||
created_at: (new Date('2020-06-22T18:45:56.000Z'))
|
created_at: (new Date('2020-06-22T18:45:56.000Z'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const message3 = {
|
const message3 = {
|
||||||
id: '9wLke9zL4Dy4OZR2RM',
|
id: '9wLke9zL4Dy4OZR2RM',
|
||||||
|
idempotency_key: '3',
|
||||||
account_id: '9vmRb29zLQReckr5ay',
|
account_id: '9vmRb29zLQReckr5ay',
|
||||||
created_at: (new Date('2020-07-22T18:45:59.000Z'))
|
created_at: (new Date('2020-07-22T18:45:59.000Z'))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue