diff --git a/src/App.js b/src/App.js index 84300e00e3..ded772fad2 100644 --- a/src/App.js +++ b/src/App.js @@ -45,8 +45,7 @@ export default { window.CSS.supports('-moz-mask-size', 'contain') || window.CSS.supports('-ms-mask-size', 'contain') || window.CSS.supports('-o-mask-size', 'contain') - ), - transitionName: 'fade' + ) }), created () { // Load the locale from the storage @@ -135,14 +134,5 @@ export default { } this.$store.dispatch('setLayoutHeight', layoutHeight) } - }, - watch: { - '$route' (to, from) { - if ((to.name === 'chat' && from.name === 'chats') || (to.name === 'chats' && from.name === 'chat')) { - this.transitionName = 'none' - } else { - this.transitionName = 'fade' - } - } } } diff --git a/src/App.scss b/src/App.scss index 29ce73a819..e2e2d079c9 100644 --- a/src/App.scss +++ b/src/App.scss @@ -47,6 +47,7 @@ html { } body { + overscroll-behavior-y: none; font-family: sans-serif; font-family: var(--interfaceFont, sans-serif); margin: 0; @@ -56,7 +57,6 @@ body { overflow-x: hidden; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - overscroll-behavior: none; &.hidden { display: none; @@ -320,7 +320,7 @@ option { i[class*=icon-] { color: $fallback--icon; - color: var(--icon, $fallback--icon) + color: var(--icon, $fallback--icon); } .btn-block { @@ -942,3 +942,38 @@ nav { max-height: 1.3rem; line-height: 1.3rem; } + +.chat-layout { + // Needed for smoother chat navigation in the desktop Safari (otherwise the chat layout "jumps" as the chat opens). + overflow: hidden; + height: 100%; + + // Ensures the fixed position of the mobile browser bars on scroll up / down events. + // Prevents the mobile browser bars from overlapping or hiding the message posting form. + @media all and (max-width: 800px) { + body { + height: 100%; + } + + #app { + height: 100%; + overflow: hidden; + min-height: auto; + } + + #app_bg_wrapper { + overflow: hidden; + } + + .main { + overflow: hidden; + height: 100%; + } + + #content { + padding-top: 0; + height: 100%; + overflow: visible; + } + } +} diff --git a/src/App.vue b/src/App.vue index 5d42993400..0276c6a60c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -113,9 +113,7 @@ {{ $t("login.hint") }} - - - + diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js index 6e23c20c2c..9c4e5b0554 100644 --- a/src/components/chat/chat.js +++ b/src/components/chat/chat.js @@ -2,29 +2,26 @@ import _ from 'lodash' import { WSConnectionStatus } from '../../services/api/api.service.js' import { mapGetters, mapState } from 'vuex' import ChatMessage from '../chat_message/chat_message.vue' -import ChatAvatar from '../chat_avatar/chat_avatar.vue' 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 ChatLayout from './chat_layout.js' import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight } from './chat_layout_utils.js' const BOTTOMED_OUT_OFFSET = 10 const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150 +const SAFE_RESIZE_TIME_OFFSET = 100 const Chat = { components: { ChatMessage, ChatTitle, - ChatAvatar, PostStatusForm }, - mixins: [ChatLayout], data () { return { jumpToBottomButtonVisible: false, hoveredMessageChainId: undefined, - scrollPositionBeforeResize: {}, + lastScrollPosition: {}, scrollableContainerHeight: '100%', errorLoadingChat: false } @@ -119,6 +116,7 @@ const Chat = { }, onFilesDropped () { this.$nextTick(() => { + this.handleResize() this.updateScrollableContainerHeight() }) }, @@ -129,13 +127,30 @@ const Chat = { } }) }, - handleLayoutChange () { - this.updateScrollableContainerHeight() - if (this.mobileLayout) { - this.setMobileChatLayout() - } else { - this.unsetMobileChatLayout() + setChatLayout () { + // This is a hacky way to adjust the global layout to the mobile chat (without modifying the rest of the app). + // This layout prevents empty spaces from being visible at the bottom + // of the chat on iOS Safari (`safe-area-inset`) when + // - the on-screen keyboard appears and the user starts typing + // - the user selects the text inside the input area + // - the user selects and deletes the text that is multiple lines long + // TODO: unify the chat layout with the global layout. + let html = document.querySelector('html') + if (html) { + html.classList.add('chat-layout') } + + this.$nextTick(() => { + this.updateScrollableContainerHeight() + }) + }, + unsetChatLayout () { + let html = document.querySelector('html') + if (html) { + html.classList.remove('chat-layout') + } + }, + handleLayoutChange () { this.$nextTick(() => { this.updateScrollableContainerHeight() this.scrollDown() @@ -149,15 +164,24 @@ const Chat = { this.scrollableContainerHeight = scrollableContainerHeight(inner, header, footer) + 'px' }, // Preserves the scroll position when OSK appears or the posting form changes its height. - handleResize (opts) { + handleResize (opts = {}) { + const { expand = false, delayed = false } = opts + + if (delayed) { + setTimeout(() => { + this.handleResize({ ...opts, delayed: false }) + }, SAFE_RESIZE_TIME_OFFSET) + return + } + this.$nextTick(() => { this.updateScrollableContainerHeight() - const { offsetHeight = undefined } = this.scrollPositionBeforeResize - this.scrollPositionBeforeResize = getScrollPosition(this.$refs.scrollable) + const { offsetHeight = undefined } = this.lastScrollPosition + this.lastScrollPosition = getScrollPosition(this.$refs.scrollable) - const diff = this.scrollPositionBeforeResize.offsetHeight - offsetHeight - if (diff < 0 || (!this.bottomedOut() && opts && opts.expand)) { + const diff = this.lastScrollPosition.offsetHeight - offsetHeight + if (diff < 0 || (!this.bottomedOut() && expand)) { this.$nextTick(() => { this.updateScrollableContainerHeight() this.$refs.scrollable.scrollTo({ @@ -281,7 +305,12 @@ const Chat = { .then(data => { this.$store.dispatch('addChatMessages', { chatId: this.currentChat.id, messages: [data] }).then(() => { this.$nextTick(() => { - this.updateScrollableContainerHeight() + 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 }) }) }) diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss index 13c52ea3b7..6ae7ebc923 100644 --- a/src/components/chat/chat.scss +++ b/src/components/chat/chat.scss @@ -3,14 +3,17 @@ height: calc(100vh - 60px); width: 100%; + .chat-title { + // prevents chat header jumping on when the user avatar loads + height: 28px; + } + .chat-view-inner { height: auto; width: 100%; overflow: visible; display: flex; - margin-top: 0.5em; - margin-left: 0.5em; - margin-right: 0.5em; + margin: 0.5em 0.5em 0 0.5em; } .chat-view-body { @@ -19,23 +22,18 @@ flex-direction: column; width: 100%; overflow: visible; - border-radius: none; min-height: 100%; - margin-left: 0; - margin-right: 0; - margin-bottom: 0em; - margin-top: 0em; + margin: 0 0 0 0; border-radius: 10px 10px 0 0; border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0 ; &::after { - border-radius: none; - box-shadow: none; + border-radius: 0; } } .scrollable-message-list { - padding: 0 10px; + padding: 0 0.8em; height: 100%; overflow-y: scroll; overflow-x: hidden; @@ -45,7 +43,7 @@ .footer { position: sticky; - bottom: 0px; + bottom: 0; } .chat-view-heading { @@ -54,15 +52,19 @@ top: 50px; display: flex; z-index: 2; - border-radius: none; position: sticky; display: flex; overflow: hidden; } .go-back-button { - margin-right: 1.2em; cursor: pointer; + margin-right: 1.4em; + + i { + display: flex; + align-items: center; + } } .jump-to-bottom-button { @@ -135,7 +137,7 @@ overflow: hidden; height: 100%; margin: 0; - border-radius: 0 !important; + border-radius: 0; } .chat-view-heading { diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue index d8c91dbe5c..62b72e14a4 100644 --- a/src/components/chat/chat.vue +++ b/src/components/chat/chat.vue @@ -75,7 +75,7 @@ :disable-polls="true" :disable-sensitivity-checkbox="true" :disable-submit="errorLoadingChat || !currentChat" - :request="sendMessage" + :post-handler="sendMessage" :submit-on-enter="!mobileLayout" :preserve-focus="!mobileLayout" :auto-focus="!mobileLayout" diff --git a/src/components/chat/chat_layout.js b/src/components/chat/chat_layout.js deleted file mode 100644 index 07ae3abfd7..0000000000 --- a/src/components/chat/chat_layout.js +++ /dev/null @@ -1,100 +0,0 @@ -const ChatLayout = { - methods: { - setChatLayout () { - if (this.mobileLayout) { - this.setMobileChatLayout() - } - }, - unsetChatLayout () { - this.unsetMobileChatLayout() - }, - setMobileChatLayout () { - // This is a hacky way to adjust the global layout to the mobile chat (without modifying the rest of the app). - // This layout prevents empty spaces from being visible at the bottom - // of the chat on iOS Safari (`safe-area-inset`) when - // - the on-screen keyboard appears and the user starts typing - // - the user selects the text inside the input area - // - the user selects and deletes the text that is multiple lines long - // TODO: unify the chat layout with the global layout. - - let html = document.querySelector('html') - if (html) { - html.style.overflow = 'hidden' - html.style.height = '100%' - } - - let body = document.querySelector('body') - if (body) { - body.style.height = '100%' - } - - let app = document.getElementById('app') - if (app) { - app.style.height = '100%' - app.style.overflow = 'hidden' - app.style.minHeight = 'auto' - } - - let appBgWrapper = window.document.getElementById('app_bg_wrapper') - if (appBgWrapper) { - appBgWrapper.style.overflow = 'hidden' - } - - let main = document.getElementsByClassName('main')[0] - if (main) { - main.style.overflow = 'hidden' - main.style.height = '100%' - } - - let content = document.getElementById('content') - if (content) { - content.style.paddingTop = '0' - content.style.height = '100%' - content.style.overflow = 'visible' - } - - this.$nextTick(() => { - this.updateScrollableContainerHeight() - }) - }, - unsetMobileChatLayout () { - let html = document.querySelector('html') - if (html) { - html.style.overflow = 'visible' - html.style.height = 'unset' - } - - let body = document.querySelector('body') - if (body) { - body.style.height = 'unset' - } - - let app = document.getElementById('app') - if (app) { - app.style.height = '100%' - app.style.overflow = 'visible' - app.style.minHeight = '100vh' - } - - let appBgWrapper = document.getElementById('app_bg_wrapper') - if (appBgWrapper) { - appBgWrapper.style.overflow = 'visible' - } - - let main = document.getElementsByClassName('main')[0] - if (main) { - main.style.overflow = 'visible' - main.style.height = 'unset' - } - - let content = document.getElementById('content') - if (content) { - content.style.paddingTop = '60px' - content.style.height = 'unset' - content.style.overflow = 'unset' - } - } - } -} - -export default ChatLayout diff --git a/src/components/chat/chat_layout_utils.js b/src/components/chat/chat_layout_utils.js index f07ba2a181..609dc0c9b8 100644 --- a/src/components/chat/chat_layout_utils.js +++ b/src/components/chat/chat_layout_utils.js @@ -22,6 +22,5 @@ export const isBottomedOut = (el, offset = 0) => { // Height of the scrollable container. The dynamic height is needed to ensure the mobile browser panel doesn't overlap or hide the posting form. export const scrollableContainerHeight = (inner, header, footer) => { - const height = parseFloat(getComputedStyle(inner, null).height.replace('px', '')) - return height - header.clientHeight - footer.clientHeight + return inner.offsetHeight - header.clientHeight - footer.clientHeight } diff --git a/src/components/chat_avatar/chat_avatar.js b/src/components/chat_avatar/chat_avatar.js deleted file mode 100644 index 7b26e07c38..0000000000 --- a/src/components/chat_avatar/chat_avatar.js +++ /dev/null @@ -1,23 +0,0 @@ -import StillImage from '../still-image/still-image.vue' -import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' -import { mapState } from 'vuex' - -const ChatAvatar = { - props: ['user', 'width', 'height'], - components: { - StillImage - }, - methods: { - getUserProfileLink (user) { - if (!user) { return } - return generateProfileLink(user.id, user.screen_name) - } - }, - computed: { - ...mapState({ - betterShadow: state => state.interface.browserSupport.cssFilter - }) - } -} - -export default ChatAvatar diff --git a/src/components/chat_avatar/chat_avatar.vue b/src/components/chat_avatar/chat_avatar.vue deleted file mode 100644 index f54a715119..0000000000 --- a/src/components/chat_avatar/chat_avatar.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - - diff --git a/src/components/chat_list_item/chat_list_item.js b/src/components/chat_list_item/chat_list_item.js index 1c27088c7e..b6b0519aa1 100644 --- a/src/components/chat_list_item/chat_list_item.js +++ b/src/components/chat_list_item/chat_list_item.js @@ -1,7 +1,7 @@ import { mapState } from 'vuex' import StatusContent from '../status_content/status_content.vue' import fileType from 'src/services/file_type/file_type.service' -import ChatAvatar from '../chat_avatar/chat_avatar.vue' +import UserAvatar from '../user_avatar/user_avatar.vue' import AvatarList from '../avatar_list/avatar_list.vue' import Timeago from '../timeago/timeago.vue' import ChatTitle from '../chat_title/chat_title.vue' @@ -12,7 +12,7 @@ const ChatListItem = { 'chat' ], components: { - ChatAvatar, + UserAvatar, AvatarList, Timeago, ChatTitle, diff --git a/src/components/chat_list_item/chat_list_item.scss b/src/components/chat_list_item/chat_list_item.scss index 12269f899f..3ec59ea2ea 100644 --- a/src/components/chat_list_item/chat_list_item.scss +++ b/src/components/chat_list_item/chat_list_item.scss @@ -1,17 +1,8 @@ .chat-list-item { - &:hover .animated.avatar { - canvas { - display: none; - } - img { - visibility: visible; - } - } - display: flex; flex-direction: row; padding: 0.75em; - height: 4.85em; + height: 5em; overflow: hidden; box-sizing: border-box; cursor: pointer; @@ -22,7 +13,7 @@ &:hover { background-color: var(--selectedPost, $fallback--lightBg); - box-shadow: 0 0px 3px 1px rgba(0, 0, 0, 0.1); + box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.1); } .chat-list-item-left { @@ -47,12 +38,6 @@ white-space: nowrap; } - .member-count { - color: $fallback--text; - color: var(--faintText, $fallback--text); - margin-right: 2px; - } - .name-and-account-name { text-overflow: ellipsis; white-space: nowrap; @@ -65,7 +50,7 @@ overflow: hidden; white-space: nowrap; text-overflow: ellipsis; - margin: 0.35rem 0; + margin: 0.35em 0; height: 1.2em; line-height: 1.2em; color: $fallback--text; @@ -78,17 +63,24 @@ pointer-events: none; } - .unread-indicator-wrapper { - display: flex; - align-items: center; - margin-left: 10px; + &:hover .animated.avatar { + canvas { + display: none; + } + img { + visibility: visible; + } } - .unread-indicator { - border-radius: 100%; - height: 8px; - width: 8px; - background-color: $fallback--link; - background-color: var(--link, $fallback--link); + .avatar.still-image { + border-radius: $fallback--avatarAltRadius; + border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + } + + .status-body { + img.emoji { + width: 1.4em; + height: 1.4em; + } } } diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue index 26ad581b27..640426b8c4 100644 --- a/src/components/chat_list_item/chat_list_item.vue +++ b/src/components/chat_list_item/chat_list_item.vue @@ -4,7 +4,7 @@ @click.capture.prevent="openChat" >
- id !== userId) }, - search: throttle(function (query) { + search (query) { if (!query) { this.loading = false return @@ -67,7 +66,7 @@ const chatNew = { this.loading = false this.userIds = data.accounts.map(a => a.id) }) - }) + } } } diff --git a/src/components/chat_new/chat_new.scss b/src/components/chat_new/chat_new.scss index 3921667724..113054443d 100644 --- a/src/components/chat_new/chat_new.scss +++ b/src/components/chat_new/chat_new.scss @@ -15,7 +15,7 @@ } .member-list { - padding-bottom: 0.67rem; + padding-bottom: 0.7rem; } .basic-user-card:hover { diff --git a/src/components/chat_title/chat_title.js b/src/components/chat_title/chat_title.js index 2723d5f555..e424bb1f05 100644 --- a/src/components/chat_title/chat_title.js +++ b/src/components/chat_title/chat_title.js @@ -1,10 +1,11 @@ import Vue from 'vue' -import ChatAvatar from '../chat_avatar/chat_avatar.vue' +import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' +import UserAvatar from '../user_avatar/user_avatar.vue' export default Vue.component('chat-title', { name: 'ChatTitle', components: { - ChatAvatar + UserAvatar }, props: [ 'user', 'withAvatar' @@ -16,5 +17,10 @@ export default Vue.component('chat-title', { htmlTitle () { return this.user ? this.user.name_html : '' } + }, + methods: { + getUserProfileLink (user) { + return generateProfileLink(user.id, user.screen_name) + } } }) diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue index fd42d125d1..cfd1e6d15f 100644 --- a/src/components/chat_title/chat_title.vue +++ b/src/components/chat_title/chat_title.vue @@ -4,16 +4,16 @@ class="chat-title" :title="title" > - - + + + diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js index a27da09053..f012344750 100644 --- a/src/components/emoji_input/emoji_input.js +++ b/src/components/emoji_input/emoji_input.js @@ -88,6 +88,11 @@ const EmojiInput = { required: false, type: String, // 'auto', 'top', 'bottom' default: 'auto' + }, + newlineOnCtrlEnter: { + required: false, + type: Boolean, + default: false } }, data () { @@ -204,7 +209,7 @@ const EmojiInput = { this.$emit('input', newValue) this.caret = 0 }, - insert ({ insertion, keepOpen }) { + insert ({ insertion, keepOpen, surroundingSpace = true }) { const before = this.value.substring(0, this.caret) || '' const after = this.value.substring(this.caret) || '' @@ -223,8 +228,8 @@ const EmojiInput = { * them, masto seem to be rendering :emoji::emoji: correctly now so why not */ const isSpaceRegex = /\s/ - const spaceBefore = !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0 ? ' ' : '' - const spaceAfter = !isSpaceRegex.exec(after[0]) && this.padEmoji ? ' ' : '' + const spaceBefore = (surroundingSpace && !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0) ? ' ' : '' + const spaceAfter = (surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji) ? ' ' : '' const newValue = [ before, @@ -381,6 +386,18 @@ const EmojiInput = { }, onKeyDown (e) { const { ctrlKey, shiftKey, key } = e + if (this.newlineOnCtrlEnter && ctrlKey && key === 'Enter') { + this.insert({ insertion: '\n', surroundingSpace: false }) + // Ensure only one new line is added on macos + e.stopPropagation() + e.preventDefault() + + // Scroll the input element to the position of the cursor + this.$nextTick(() => { + this.input.elm.blur() + this.input.elm.focus() + }) + } // Disable suggestions hotkeys if suggestions are hidden if (!this.temporarilyHideSuggestions) { if (key === 'Tab') { diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue index d719eae152..c8865d7773 100644 --- a/src/components/media_upload/media_upload.vue +++ b/src/components/media_upload/media_upload.vue @@ -33,19 +33,6 @@ @import '../../_variables.scss'; .media-upload { - &.disabled { - .new-icon { - cursor: not-allowed; - } - - &:hover { - i, label { - color: $fallback--faint; - color: var(--faint, $fallback--faint); - } - } - } - .label { display: inline-block; } diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 90d0fa81ee..59e4dc26fb 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -43,7 +43,7 @@ const PostStatusForm = { 'disableSubmit', 'placeholder', 'maxHeight', - 'request', + 'postHandler', 'preserveFocus', 'autoFocus', 'fileLimit', @@ -221,10 +221,6 @@ const PostStatusForm = { event.stopPropagation() event.preventDefault() } - if (opts.control && this.submitOnEnter) { - newStatus.status = `${newStatus.status}\n` - return - } if (this.emptyStatus) { this.error = this.$t('post_status.empty_status_error') @@ -259,9 +255,9 @@ const PostStatusForm = { poll } - const request = this.request ? this.request : statusPoster.postStatus + const postHandler = this.postHandler ? this.postHandler : statusPoster.postStatus - request(postingOptions).then((data) => { + postHandler(postingOptions).then((data) => { if (!data.error) { this.newStatus = { status: '', @@ -345,11 +341,7 @@ const PostStatusForm = { }, addMediaFile (fileInfo) { this.newStatus.files.push(fileInfo) - - // TODO: use fixed dimensions instead so relying on timeout - setTimeout(() => { - this.$emit('resize') - }, 150) + this.$emit('resize', { delayed: true }) }, removeMediaFile (fileInfo) { let index = this.newStatus.files.indexOf(fileInfo) @@ -364,6 +356,7 @@ const PostStatusForm = { this.uploadingFiles = true }, finishedUploadingFiles () { + this.$emit('resize') this.uploadingFiles = false }, type (fileInfo) { @@ -417,7 +410,7 @@ const PostStatusForm = { // Reset to default height for empty form, nothing else to do here. if (target.value === '') { target.style.height = null - this.$emit('resize', null) + this.$emit('resize') this.$refs['emoji-input'].resize() return } diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index d8df68d666..7454958ba4 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -131,6 +131,7 @@ class="form-control main-input" enable-emoji-picker hide-emoji-button + :newline-on-ctrl-enter="submitOnEnter" enable-sticker-picker @input="onEmojiInputInput" @sticker-uploaded="addMediaFile" @@ -146,8 +147,8 @@ class="form-post-body" :class="{ 'scrollable-form': !!maxHeight }" @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)" - @keydown.meta.enter="postStatus($event, newStatus, { control: true })" - @keydown.ctrl.enter="postStatus($event, newStatus)" + @keydown.meta.enter="postStatus($event, newStatus)" + @keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)" @input="resize" @compositionupdate="resize" @paste="paste" @@ -435,6 +436,19 @@ color: var(--lightText, $fallback--lightText); } } + + &.disabled { + i { + cursor: not-allowed; + color: $fallback--icon; + color: var(--btnDisabledText, $fallback--icon); + + &:hover { + color: $fallback--icon; + color: var(--btnDisabledText, $fallback--icon); + } + } + } } // Order is not necessary but a good indicator @@ -628,7 +642,7 @@ } // todo: unify with attachment.vue (otherwise the uploaded images are not minified unless a status with an attachment was displayed before) -img.media-upload { +img.media-upload, .media-upload-container > video { line-height: 0; max-height: 200px; max-width: 100%; diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js index 763a76070c..b60a889bd0 100644 --- a/src/services/chat_service/chat_service.js +++ b/src/services/chat_service/chat_service.js @@ -31,7 +31,8 @@ const deleteMessage = (storage, messageId) => { } if (storage.minId === messageId) { - storage.minId = _.minBy(storage.messages, 'id') + const firstMessage = _.minBy(storage.messages, 'id') + storage.minId = firstMessage.id } } @@ -73,12 +74,12 @@ const getView = (storage) => { const result = [] const messages = _.sortBy(storage.messages, ['id', 'desc']) - const firstMessages = messages[0] - let prev = messages[messages.length - 1] + const firstMessage = messages[0] + let previousMessage = messages[messages.length - 1] let currentMessageChainId - if (firstMessages) { - const date = new Date(firstMessages.created_at) + if (firstMessage) { + const date = new Date(firstMessage.created_at) date.setHours(0, 0, 0, 0) result.push({ type: 'date', @@ -97,14 +98,14 @@ const getView = (storage) => { date.setHours(0, 0, 0, 0) // insert date separator and start a new message chain - if (prev && prev.date < date) { + if (previousMessage && previousMessage.date < date) { result.push({ type: 'date', date, id: date.getTime().toString() }) - prev['isTail'] = true + previousMessage['isTail'] = true currentMessageChainId = undefined afterDate = true } @@ -124,14 +125,14 @@ const getView = (storage) => { } // start a new message chain - if ((prev && prev.data && prev.data.account_id) !== message.account_id || afterDate) { + if ((previousMessage && previousMessage.data && previousMessage.data.account_id) !== message.account_id || afterDate) { currentMessageChainId = _.uniqueId() object['isHead'] = true object['messageChainId'] = currentMessageChainId } result.push(object) - prev = object + previousMessage = object afterDate = false } diff --git a/test/unit/specs/services/chat_service/chat_service.spec.js b/test/unit/specs/services/chat_service/chat_service.spec.js new file mode 100644 index 0000000000..4e8e566b6a --- /dev/null +++ b/test/unit/specs/services/chat_service/chat_service.spec.js @@ -0,0 +1,89 @@ +import chatService from '../../../../../src/services/chat_service/chat_service.js' + +const message1 = { + id: '9wLkdcmQXD21Oy8lEX', + created_at: (new Date('2020-06-22T18:45:53.000Z')) +} + +const message2 = { + id: '9wLkdp6ihaOVdNj8Wu', + account_id: '9vmRb29zLQReckr5ay', + created_at: (new Date('2020-06-22T18:45:56.000Z')) +} + +const message3 = { + id: '9wLke9zL4Dy4OZR2RM', + account_id: '9vmRb29zLQReckr5ay', + created_at: (new Date('2020-07-22T18:45:59.000Z')) +} + +// TODO: only +describe.only('chatService', () => { + describe('.add', () => { + it("Doesn't add duplicates", () => { + const chat = chatService.empty() + chatService.add(chat, { messages: [ message1 ] }) + chatService.add(chat, { messages: [ message1 ] }) + expect(chat.messages.length).to.eql(1) + + chatService.add(chat, { messages: [ message2 ] }) + expect(chat.messages.length).to.eql(2) + }) + + it('Updates minId and lastMessage and newMessageCount', () => { + const chat = chatService.empty() + + chatService.add(chat, { messages: [ message1 ] }) + expect(chat.lastMessage.id).to.eql(message1.id) + expect(chat.minId).to.eql(message1.id) + expect(chat.newMessageCount).to.eql(1) + + chatService.add(chat, { messages: [ message2 ] }) + expect(chat.lastMessage.id).to.eql(message2.id) + expect(chat.minId).to.eql(message1.id) + expect(chat.newMessageCount).to.eql(2) + + chatService.resetNewMessageCount(chat) + expect(chat.newMessageCount).to.eql(0) + + const createdAt = new Date() + createdAt.setSeconds(createdAt.getSeconds() + 10) + chatService.add(chat, { messages: [ { message3, created_at: createdAt } ] }) + expect(chat.newMessageCount).to.eql(1) + }) + }) + + describe('.delete', () => { + it('Updates minId and lastMessage', () => { + const chat = chatService.empty() + + chatService.add(chat, { messages: [ message1 ] }) + chatService.add(chat, { messages: [ message2 ] }) + chatService.add(chat, { messages: [ message3 ] }) + + expect(chat.lastMessage.id).to.eql(message3.id) + expect(chat.minId).to.eql(message1.id) + + chatService.deleteMessage(chat, message3.id) + expect(chat.lastMessage.id).to.eql(message2.id) + expect(chat.minId).to.eql(message1.id) + + chatService.deleteMessage(chat, message1.id) + expect(chat.lastMessage.id).to.eql(message2.id) + expect(chat.minId).to.eql(message2.id) + }) + }) + + describe('.getView', () => { + it('Inserts date separators', () => { + const chat = chatService.empty() + + chatService.add(chat, { messages: [ message1 ] }) + chatService.add(chat, { messages: [ message2 ] }) + chatService.add(chat, { messages: [ message3 ] }) + + const view = chatService.getView(chat) + expect(view.map(i => i.type)).to.eql(['date', 'message', 'message', 'date', 'message']) + }) + }) +})