Merge remote-tracking branch 'origin/develop' into shigusegubu

* origin/develop:
  Fix the chat scroll behavior for vertical screens.
  fix mobile badge alignment
  change approach to disable all, enable some
  fix mobile navbar hitboxes
  Instead of blocking all interaction, only block interaction in places that matter
  added comment
  fix chat heading not being aligned and using wrong styles
  fix chat badge and unify styles across all badges including follow request count.
  change logo to svg
  Update CHANGELOG.md
  update changelog for optimistic chat posting
  fix test
  move from using timestamps to ids when tracking last seen in chats
  Apply 1 suggestion(s) to 1 file(s)
  Optimistic message sending for chat
This commit is contained in:
Henry Jameson 2020-11-08 14:56:51 +02:00
commit 893741c363
26 changed files with 309 additions and 102 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,6 +21,12 @@
/>
</span>
<span class="heading-right" />
<div class="time-wrapper">
<Timeago
:time="chat.updated_at"
:auto-update="60"
/>
</div>
</div>
<div class="chat-preview">
<StatusContent
@ -35,12 +41,6 @@
</div>
</div>
</div>
<div class="time-wrapper">
<Timeago
:time="chat.updated_at"
:auto-update="60"
/>
</div>
</div>
</template>

View file

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

View file

@ -32,7 +32,7 @@
>
<div
class="media status"
:class="{ 'without-attachment': !hasAttachment }"
:class="{ 'without-attachment': !hasAttachment, 'pending': chatViewItem.data.pending, 'error': chatViewItem.data.error }"
style="position: relative"
@mouseenter="hovered = true"
@mouseleave="hovered = false"

View file

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

View file

@ -27,7 +27,7 @@
<router-link :to="{ name: 'chats', params: { username: currentUser.screen_name } }">
<div
v-if="unreadChatCount"
class="badge badge-notification unread-chat-count"
class="badge badge-notification"
>
{{ unreadChatCount }}
</div>
@ -47,7 +47,7 @@
/>{{ $t("nav.friend_requests") }}
<span
v-if="followRequestCount > 0"
class="badge follow-request-count"
class="badge badge-notification"
>
{{ followRequestCount }}
</span>
@ -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;
}
}
</style>

View file

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

View file

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

View file

@ -70,7 +70,7 @@
/> {{ $t("nav.chats") }}
<span
v-if="unreadChatCount"
class="badge badge-notification unread-chat-count"
class="badge badge-notification"
>
{{ unreadChatCount }}
</span>
@ -99,7 +99,7 @@
/> {{ $t("nav.friend_requests") }}
<span
v-if="followRequestCount > 0"
class="badge follow-request-count"
class="badge badge-notification"
>
{{ followRequestCount }}
</span>
@ -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 {

View file

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

View file

@ -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'] : []),

View file

@ -113,7 +113,7 @@
}
&.-blocked {
pointer-events: none;
cursor: progress;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View file

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