From 804cf3abc53f3108cf0f7f92db18dc1ad733fdec Mon Sep 17 00:00:00 2001
From: eugenijm
Date: Thu, 7 May 2020 16:10:53 +0300
Subject: [PATCH 01/41] WIP: Chats
---
src/App.js | 12 +-
src/App.scss | 22 ++
src/App.vue | 3 +-
src/_variables.scss | 1 +
src/boot/routes.js | 4 +
.../account_actions/account_actions.js | 6 +
.../account_actions/account_actions.vue | 6 +
src/components/chat/chat.js | 337 ++++++++++++++++++
src/components/chat/chat.scss | 192 ++++++++++
src/components/chat/chat.vue | 90 +++++
src/components/chat_avatar/chat_avatar.js | 34 ++
src/components/chat_avatar/chat_avatar.vue | 127 +++++++
src/components/chat_list/chat_list.js | 44 +++
src/components/chat_list/chat_list.vue | 56 +++
.../chat_list_item/chat_list_item.js | 38 ++
.../chat_list_item/chat_list_item.scss | 156 ++++++++
.../chat_list_item/chat_list_item.vue | 48 +++
src/components/chat_message/chat_message.js | 62 ++++
src/components/chat_message/chat_message.scss | 171 +++++++++
src/components/chat_message/chat_message.vue | 41 +++
.../chat_message_date/chat_message_date.vue | 24 ++
src/components/chat_new/chat_new.js | 74 ++++
src/components/chat_new/chat_new.scss | 87 +++++
src/components/chat_new/chat_new.vue | 50 +++
src/components/chat_title/chat_title.js | 39 ++
src/components/chat_title/chat_title.vue | 41 +++
.../mobile_post_status_button.js | 7 +
src/components/nav_panel/nav_panel.vue | 5 +
src/components/notification/notification.js | 13 +-
src/components/notification/notification.vue | 22 ++
.../post_status_form/post_status_form.js | 37 +-
.../post_status_form/post_status_form.vue | 19 +-
src/components/side_drawer/side_drawer.vue | 3 +
.../style_switcher/style_switcher.js | 6 +-
.../style_switcher/style_switcher.vue | 65 ++++
src/hocs/with_load_more/with_load_more.scss | 4 +
src/i18n/en.json | 27 +-
src/main.js | 4 +-
src/modules/chats.js | 163 +++++++++
src/modules/config.js | 3 +-
src/modules/statuses.js | 12 +-
src/services/api/api.service.js | 67 +++-
src/services/chat_service/chat_service.js | 100 ++++++
.../entity_normalizer.service.js | 19 +-
.../notification_utils/notification_utils.js | 3 +-
src/services/style_setter/style_setter.js | 3 +-
src/services/theme_data/pleromafe.js | 46 +++
static/fontello.json | 0
static/themes/breezy-dark.json | 3 +-
static/themes/redmond-xx-se.json | 3 +-
static/themes/redmond-xx.json | 3 +-
static/themes/redmond-xxi.json | 3 +-
52 files changed, 2374 insertions(+), 31 deletions(-)
create mode 100644 src/components/chat/chat.js
create mode 100644 src/components/chat/chat.scss
create mode 100644 src/components/chat/chat.vue
create mode 100644 src/components/chat_avatar/chat_avatar.js
create mode 100644 src/components/chat_avatar/chat_avatar.vue
create mode 100644 src/components/chat_list/chat_list.js
create mode 100644 src/components/chat_list/chat_list.vue
create mode 100644 src/components/chat_list_item/chat_list_item.js
create mode 100644 src/components/chat_list_item/chat_list_item.scss
create mode 100644 src/components/chat_list_item/chat_list_item.vue
create mode 100644 src/components/chat_message/chat_message.js
create mode 100644 src/components/chat_message/chat_message.scss
create mode 100644 src/components/chat_message/chat_message.vue
create mode 100644 src/components/chat_message_date/chat_message_date.vue
create mode 100644 src/components/chat_new/chat_new.js
create mode 100644 src/components/chat_new/chat_new.scss
create mode 100644 src/components/chat_new/chat_new.vue
create mode 100644 src/components/chat_title/chat_title.js
create mode 100644 src/components/chat_title/chat_title.vue
create mode 100644 src/modules/chats.js
create mode 100644 src/services/chat_service/chat_service.js
mode change 100755 => 100644 static/fontello.json
diff --git a/src/App.js b/src/App.js
index bbb41409..6a8e74a6 100644
--- a/src/App.js
+++ b/src/App.js
@@ -41,7 +41,8 @@ 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
@@ -124,5 +125,14 @@ export default {
this.$store.dispatch('setMobileLayout', mobileLayout)
}
}
+ },
+ 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 89aa3215..c39a3b07 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -969,3 +969,25 @@ nav {
background-color: $fallback--fg;
background-color: var(--panel, $fallback--fg);
}
+
+.alert-dot-number {
+ display: inline-block;
+ border-radius: 1em;
+ min-width: 1.3rem;
+ min-height: 1.3rem;
+ max-height: 1.3rem;
+ font-size: 0.9em;
+ font-weight: bolder;
+ line-height: 1.3rem;
+ text-align: center;
+ vertical-align: middle;
+ white-space: nowrap;
+ padding: 0 0.3em;
+ position: absolute;
+ right: 0.6rem;
+ background-color: $fallback--cRed;
+ background-color: var(--badgeNotification, $fallback--cRed);
+ color: white;
+ color: var(--badgeNotificationText, white);
+ font-style: normal;
+}
diff --git a/src/App.vue b/src/App.vue
index 7018a5a4..fe353eb5 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -76,6 +76,7 @@
+
-
+
diff --git a/src/_variables.scss b/src/_variables.scss
index 30dc3e42..9004d551 100644
--- a/src/_variables.scss
+++ b/src/_variables.scss
@@ -27,5 +27,6 @@ $fallback--tooltipRadius: 5px;
$fallback--avatarRadius: 4px;
$fallback--avatarAltRadius: 10px;
$fallback--attachmentRadius: 10px;
+$fallback--chatMessageRadius: 10px;
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
diff --git a/src/boot/routes.js b/src/boot/routes.js
index 7400a682..48b8c9da 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -5,6 +5,8 @@ import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
import ConversationPage from 'components/conversation-page/conversation-page.vue'
import Interactions from 'components/interactions/interactions.vue'
import DMs from 'components/dm_timeline/dm_timeline.vue'
+import ChatList from 'components/chat_list/chat_list.vue'
+import Chat from 'components/chat/chat.vue'
import UserProfile from 'components/user_profile/user_profile.vue'
import Search from 'components/search/search.vue'
import Settings from 'components/settings/settings.vue'
@@ -56,6 +58,8 @@ export default (store) => {
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
+ { name: 'chat', path: '/chats/:recipient_id', component: Chat, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute },
+ { name: 'chats', path: '/users/:username/chats', component: ChatList, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute },
{ name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration },
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js
index 0826c275..9ad8bd1d 100644
--- a/src/components/account_actions/account_actions.js
+++ b/src/components/account_actions/account_actions.js
@@ -27,6 +27,12 @@ const AccountActions = {
},
reportUser () {
this.$store.dispatch('openUserReportingModal', this.user.id)
+ },
+ openChat () {
+ this.$router.push({
+ name: 'chat',
+ params: { recipient_id: this.user.id }
+ })
}
}
}
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
index 744b77d5..308f1d6e 100644
--- a/src/components/account_actions/account_actions.vue
+++ b/src/components/account_actions/account_actions.vue
@@ -49,6 +49,12 @@
>
{{ $t('user_card.report') }}
+
{
+ let scrollable = this.$refs.scrollable
+ if (scrollable) {
+ window.addEventListener('scroll', this.handleScroll)
+ }
+ this.updateSize()
+ })
+ if (this.isMobileLayout) {
+ this.setMobileChatLayout()
+ }
+
+ if (typeof document.hidden !== 'undefined') {
+ document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
+ this.$store.commit('setChatFocused', !document.hidden)
+ }
+ },
+ destroyed () {
+ window.removeEventListener('scroll', this.handleScroll)
+ window.removeEventListener('resize', this.handleLayoutChange)
+ this.unsetMobileChatLayout()
+ if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
+ this.$store.dispatch('clearCurrentChat')
+ },
+ computed: {
+ chatParticipants () {
+ if (this.currentChat) {
+ return [this.currentChat.account]
+ } else {
+ return []
+ }
+ },
+ recipient () {
+ return this.currentChat && this.currentChat.account
+ },
+ formPlaceholder () {
+ if (this.recipient) {
+ return this.$t('chats.message_user', { nickname: this.recipient.screen_name })
+ } else {
+ return this.$t('chats.write_message')
+ }
+ },
+ ...mapGetters(['currentChat', 'currentChatMessageService', 'findUser']),
+ ...mapState({
+ backendInteractor: state => state.api.backendInteractor,
+ currentUser: state => state.users.currentUser,
+ isMobileLayout: state => state.interface.mobileLayout
+ })
+ },
+ watch: {
+ chatViewItems (prev, next) {
+ let bottomedOut = this.bottomedOut(10)
+ this.$nextTick(() => {
+ if (bottomedOut && prev.length !== next.length) {
+ this.newMessageCount = this.currentChatMessageService.newMessageCount
+ this.scrollDown({ forceRead: true })
+ }
+ })
+ },
+ '$route': function (prev, next) {
+ this.recipientId = this.$route.params.recipient_id
+ this.startFetching()
+ }
+ },
+ methods: {
+ onStatusHover ({ state, sequenceId }) {
+ this.hoveredSequenceId = state ? sequenceId : undefined
+ },
+ onPosted (data) {
+ this.$store.dispatch('addChatMessages', { chatId: this.currentChat.id, messages: [data] }).then(() => {
+ this.chatViewItems = chatService.getView(this.currentChatMessageService)
+ this.updateSize()
+ this.scrollDown({ forceRead: true })
+ })
+ },
+ onScopeNoticeDismissed () {
+ this.$nextTick(() => {
+ this.updateSize()
+ })
+ },
+ onFilesDropped () {
+ this.$nextTick(() => {
+ this.updateSize()
+ })
+ },
+ handleVisibilityChange () {
+ this.$store.commit('setChatFocused', !document.hidden)
+ },
+ handleLayoutChange () {
+ this.updateSize()
+ let mobileLayout = this.isMobileLayout
+ if (this.mobileLayout !== mobileLayout) {
+ if (this.mobileLayout === false && mobileLayout === true) {
+ this.setMobileChatLayout()
+ }
+ if (this.mobileLayout === true && mobileLayout === false) {
+ this.unsetMobileChatLayout()
+ }
+ this.mobileLayout = this.isMobileLayout
+ this.$nextTick(() => {
+ this.updateSize()
+ this.scrollDown()
+ })
+ }
+ },
+ 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%'
+ body.style.overscrollBehavior = 'none'
+ }
+
+ 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.updateSize()
+ })
+ },
+ 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'
+ body.style.overscrollBehavior = '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'
+ }
+ },
+ handleResize (newHeight) {
+ this.updateSize(newHeight)
+ },
+ updateSize (newHeight, _diff) {
+ let h = this.$refs.header
+ let s = this.$refs.scrollable
+ let f = this.$refs.footer
+ if (h && s && f) {
+ let height = 0
+ if (this.isMobileLayout) {
+ height = parseFloat(getComputedStyle(window.document.body, null).height.replace('px', ''))
+ let newHeight = (height - h.clientHeight - f.clientHeight)
+ s.style.height = newHeight + 'px'
+ } else {
+ height = parseFloat(getComputedStyle(this.$refs.inner, null).height.replace('px', ''))
+ let newHeight = (height - h.clientHeight - f.clientHeight)
+ s.style.height = newHeight + 'px'
+ }
+ }
+ },
+ scrollDown (options = {}) {
+ let { behavior = 'auto', forceRead = false } = options
+ let container = this.$refs.scrollable
+ let scrollable = this.$refs.scrollable
+ this.doScrollDown(scrollable, container, behavior)
+ if (forceRead || this.newMessageCount > 0) {
+ this.readChat()
+ }
+ },
+ doScrollDown (scrollable, container, behavior) {
+ if (!container) { return }
+ this.$nextTick(() => {
+ scrollable.scrollTo({ top: container.scrollHeight, left: 0, behavior })
+ })
+ },
+ bottomedOut (offset) {
+ let bottomedOut = false
+
+ if (this.$refs.scrollable) {
+ let scrollHeight = this.$refs.scrollable.scrollTop + (offset || 0)
+ let totalHeight = this.$refs.scrollable.scrollHeight - this.$refs.scrollable.offsetHeight
+ bottomedOut = totalHeight <= scrollHeight
+ }
+
+ return bottomedOut
+ },
+ handleScroll: throttle(function () {
+ if (this.bottomedOut(150)) {
+ this.jumpToBottomButtonVisible = false
+ let newMessageCount = this.newMessageCount
+ if (newMessageCount > 0) {
+ this.readChat()
+ }
+ } else {
+ this.jumpToBottomButtonVisible = true
+ }
+ }, 100),
+ goBack () {
+ this.$router.push({ name: 'chats', params: { username: this.screen_name } })
+ },
+ fetchChat (isFirstFetch, chatId) {
+ this.chatViewItems = chatService.getView(this.currentChatMessageService)
+ if (isFirstFetch) {
+ this.scrollDown({ forceRead: true })
+ }
+ this.backendInteractor.chatMessages({ id: chatId })
+ .then((messages) => {
+ let bottomedOut = this.bottomedOut()
+ this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => {
+ this.chatViewItems = chatService.getView(this.currentChatMessageService)
+ this.newMessageCount = this.currentChatMessageService.newMessageCount
+ if (isFirstFetch) {
+ this.$nextTick(() => {
+ this.updateSize()
+ })
+ } else if (bottomedOut) {
+ this.scrollDown()
+ }
+ setTimeout(() => {
+ this.loadingMessages = false
+ }, 1000)
+ })
+ })
+ },
+ readChat () {
+ if (!this.currentChat.id) { return }
+ this.$store.dispatch('readChat', { id: this.currentChat.id })
+ this.newMessageCount = this.currentChatMessageService.newMessageCount
+ },
+ async startFetching () {
+ const chat = await this.backendInteractor.getOrCreateChat({ accountId: this.recipientId })
+ this.$store.dispatch('addOpenedChat', { chat })
+ this.doStartFetching()
+ },
+ doStartFetching () {
+ let chatId = this.currentChat.id
+ this.$store.dispatch('startFetchingCurrentChat', {
+ fetcher: () => setInterval(() => this.fetchChat(false, chatId), 5000)
+ })
+ this.fetchChat(true, chatId)
+ },
+ poster ({ status }) {
+ return this.backendInteractor.postChatMessage({
+ id: this.currentChat.id,
+ content: status
+ })
+ }
+ }
+}
+
+export default Chat
diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss
new file mode 100644
index 00000000..25e27cf5
--- /dev/null
+++ b/src/components/chat/chat.scss
@@ -0,0 +1,192 @@
+.direct-conversation-view {
+ display: flex;
+ height: calc(100vh - 60px);
+ width: 100%;
+
+ .direct-conversation-view-inner {
+ height: auto;
+ width: 100%;
+ overflow: visible;
+ display: flex;
+ margin-top: 0.5em;
+ margin-left: 0.5em;
+ margin-right: 0.5em;
+
+ .direct-conversation-view-body {
+ background-color: var(--chatBg, $fallback--bg);
+ display: flex;
+ 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;
+ border-radius: 10px 10px 0 0;
+ border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0 ;
+
+ &.panel {
+ &::after {
+ border-radius: 0;
+ box-shadow: none;
+ }
+ }
+
+ .direct-conversation-view-heading {
+ align-items: center;
+ justify-content: space-between;
+ top: 50px;
+ display: flex;
+ z-index: 2;
+ border-radius: none;
+ position: -webkit-sticky;
+ position: sticky;
+
+ .go-back-button {
+ cursor: pointer;
+ margin-right: 0.7em;
+
+ i {
+ color: $fallback--link;
+ color: var(--panelLink, $fallback--link);
+ }
+ }
+
+ .title {
+ flex-shrink: 1;
+ margin-right: 0em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ line-height: 28px;
+
+ flex-shrink: 1;
+ margin-right: 0em;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex-shrink: 0;
+ max-width: 70%;
+ display: grid;
+ }
+ }
+
+ .scrollable {
+ padding: 0 10px;
+ height: 100%;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .footer {
+ position: -webkit-sticky;
+ position: sticky;
+ bottom: 0px;
+ }
+ }
+ }
+}
+
+@media all and (max-width: 800px) {
+ .direct-conversation-view {
+ height: 100%;
+ overflow: hidden;
+
+ .direct-conversation-view-inner {
+ overflow: hidden;
+ height: 100%;
+ margin-top: 0;
+ margin-left: 0;
+ margin-right: 0;
+
+ .direct-conversation-view-body {
+ display: flex;
+ min-height: auto;
+ overflow: hidden;
+ height: 100%;
+ margin: 0;
+ border-radius: 0 !important;
+
+ .direct-conversation-view-heading {
+ position: static;
+ z-index: 9999;
+ top: 0;
+ margin-top: 0;
+ border-radius: 0;
+ }
+
+ .scrollable {
+ display: unset;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .footer {
+ position: relative;
+ bottom: auto;
+
+ .post-status-form form {
+ padding: 0;
+ }
+ }
+ }
+ }
+ }
+}
+
+.jump-to-bottom-button {
+ width: 2.5em;
+ height: 2.5em;
+ border-radius: 100%;
+ position: absolute;
+ position: absolute;
+ right: 1.3em;
+ top: -3.2em;
+ background-color: $fallback--fg;
+ background-color: var(--btn, $fallback--fg);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3), 0px 2px 4px rgba(0, 0, 0, 0.3);
+ z-index: 10;
+
+ transition: 0.35s all;
+ transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
+
+ opacity: 0;
+ visibility: hidden;
+ cursor: pointer;
+
+ &.visible {
+ opacity: 1;
+ visibility: visible;
+ }
+
+ i {
+ font-size: 1em;
+ color: $fallback--text;
+ color: var(--text, $fallback--text);
+ }
+
+ .new-messages-alert-dot {
+ left: 50%;
+ transform: translate(-50%, 0);
+ border-radius: 100%;
+ height: 1.3em;
+ width: 1.3em;
+ position: absolute;
+ top: calc(50% - 8px);
+ text-align: center;
+ font-style: normal;;
+ font-weight: bolder;
+ margin-top: -1rem;
+ font-size: 0.8em;
+ background-color: $fallback--cRed;
+ background-color: var(--badgeNotification, $fallback--cRed);
+ color: white;
+ color: var(--badgeNotificationText, white);
+ }
+}
diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue
new file mode 100644
index 00000000..02b03609
--- /dev/null
+++ b/src/components/chat/chat.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
diff --git a/src/components/chat_avatar/chat_avatar.js b/src/components/chat_avatar/chat_avatar.js
new file mode 100644
index 00000000..8603a2cc
--- /dev/null
+++ b/src/components/chat_avatar/chat_avatar.js
@@ -0,0 +1,34 @@
+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: ['users', 'fallbackUser', 'width', 'height'],
+ components: {
+ StillImage
+ },
+ methods: {
+ getUserProfileLink (user) {
+ return generateProfileLink(user.id, user.screen_name)
+ }
+ },
+ computed: {
+ firstUser () {
+ return this.users[0] || this.fallbackUser
+ },
+ secondUser () {
+ return this.users[1]
+ },
+ thirdUser () {
+ return this.users[2]
+ },
+ fourthUser () {
+ return this.users[3]
+ },
+ ...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
new file mode 100644
index 00000000..f76039e2
--- /dev/null
+++ b/src/components/chat_avatar/chat_avatar.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/chat_list/chat_list.js b/src/components/chat_list/chat_list.js
new file mode 100644
index 00000000..d150cc96
--- /dev/null
+++ b/src/components/chat_list/chat_list.js
@@ -0,0 +1,44 @@
+import { mapState } from 'vuex'
+import ChatListItem from '../chat_list_item/chat_list_item.vue'
+import ChatNew from '../chat_new/chat_new.vue'
+import List from '../list/list.vue'
+import withLoadMore from '../../hocs/with_load_more/with_load_more'
+
+const Chats = withLoadMore({
+ fetch: (props, $store) => $store.dispatch('fetchChats'),
+ select: (props, $store) => $store.state.chats.chatList.data,
+ destroy: (props, $store) => undefined,
+ childPropName: 'items'
+})(List)
+
+const ChatList = {
+ components: {
+ ChatListItem,
+ Chats,
+ ChatNew
+ },
+ computed: {
+ ...mapState({
+ currentUser: state => state.users.currentUser
+ })
+ },
+ data () {
+ return {
+ isNew: false
+ }
+ },
+ created () {
+ this.$store.dispatch('fetchChats', { reset: true })
+ },
+ methods: {
+ cancelNewChat () {
+ this.isNew = false
+ this.$store.dispatch('fetchChats', { reset: true })
+ },
+ newChat () {
+ this.isNew = true
+ }
+ }
+}
+
+export default ChatList
diff --git a/src/components/chat_list/chat_list.vue b/src/components/chat_list/chat_list.vue
new file mode 100644
index 00000000..a88c87a1
--- /dev/null
+++ b/src/components/chat_list/chat_list.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+ {{ $t("chats.chats") }}
+
+ {{ ' ' }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/chat_list_item/chat_list_item.js b/src/components/chat_list_item/chat_list_item.js
new file mode 100644
index 00000000..72399782
--- /dev/null
+++ b/src/components/chat_list_item/chat_list_item.js
@@ -0,0 +1,38 @@
+import { mapState } from 'vuex'
+import ChatAvatar from '../chat_avatar/chat_avatar.vue'
+import AvatarList from '../avatar_list/avatar_list.vue'
+import Timeago from '../timeago/timeago.vue'
+import ChatTitle from '../chat_title/chat_title.vue'
+
+const ChatListItem = {
+ name: 'ChatListItem',
+ props: [
+ 'chat'
+ ],
+ components: {
+ ChatAvatar,
+ AvatarList,
+ Timeago,
+ ChatTitle
+ },
+ computed: {
+ ...mapState({
+ currentUser: state => state.users.currentUser
+ })
+ },
+ methods: {
+ openChat (_e) {
+ if (this.chat.id) {
+ this.$router.push({
+ name: 'chat',
+ params: {
+ username: this.currentUser.screen_name,
+ recipient_id: this.chat.account.id
+ }
+ })
+ }
+ }
+ }
+}
+
+export default ChatListItem
diff --git a/src/components/chat_list_item/chat_list_item.scss b/src/components/chat_list_item/chat_list_item.scss
new file mode 100644
index 00000000..ee9054bb
--- /dev/null
+++ b/src/components/chat_list_item/chat_list_item.scss
@@ -0,0 +1,156 @@
+.chat-list-item {
+ &:hover .animated.avatar {
+ canvas {
+ display: none;
+ }
+ img {
+ visibility: visible;
+ }
+ }
+
+ display: flex;
+ flex-direction: row;
+
+ padding: 0.75em;
+ height: 4.85em;
+ overflow: hidden;
+ box-sizing: border-box;
+ cursor: pointer;
+
+ :focus {
+ outline: none;
+ }
+
+ &:hover {
+ background-color: var(--selectedPost, $fallback--lightBg);
+ box-shadow: 0 0px 3px 1px rgba(0, 0, 0, 0.1);
+ }
+
+ .chat-list-item-left {
+ margin-right: 1em;
+ }
+
+ .chat-list-item-center {
+ width: 100%;
+ box-sizing: border-box;
+ overflow: hidden;
+ word-wrap: break-word;
+
+ .chat-preview {
+ display: inline-flex;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ margin: 0.35rem 0;
+ height: 16px;
+ color: $fallback--text;
+ color: var(--faintText, $fallback--text);
+ width: 100%;
+ justify-content: space-between;
+ line-height: 1em;
+
+ .unread-indicator-wrapper {
+ display: flex;
+ align-items: center;
+ margin-left: 10px;
+
+ .unread-indicator {
+ border-radius: 100%;
+ height: 8px;
+ width: 8px;
+ background-color: $fallback--link;
+ background-color: var(--link, $fallback--link);
+ }
+ }
+
+ .content {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ flex: 1;
+ margin-right: 10px;
+ }
+
+ .faint-link {
+ color: var(--faintLink, $fallback--link);
+ text-decoration: none;
+ }
+
+ .account-name {
+ min-width: 1.6em;
+ margin-right: 0.2em;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: var(--faintLink, $fallback--link);
+ }
+
+ .user-name {
+ margin-right: 0.4em;
+ flex-shrink: 1;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 14px;
+ overflow: hidden;
+ flex-shrink: 0;
+ max-width: 55%;
+ font-weight: bold;
+
+ img {
+ width: 14px;
+ height: 14px;
+ vertical-align: middle;
+ object-fit: contain
+ }
+ }
+
+ a {
+ color: $fallback--link;
+ color: var(--faintLink, $fallback--link);
+ }
+
+ p {
+ margin: 0;
+ display: inline;
+ word-wrap: break-word;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ img, video {
+ max-width: 100%;
+ max-height: 400px;
+ vertical-align: middle;
+
+ &.emoji {
+ width: 1.125rem;
+ height: 1.125rem;
+ }
+ }
+ }
+
+ .heading {
+ width: 100%;
+ display: inline-flex;
+ justify-content: space-between;
+ line-height: 1em;
+
+ .heading-right {
+ 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;
+ overflow: hidden;
+ flex-shrink: 1;
+ }
+ }
+ }
+}
diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue
new file mode 100644
index 00000000..1acc49a5
--- /dev/null
+++ b/src/components/chat_list_item/chat_list_item.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'Last message placeholder' }}
+
+ {{ chat.unread }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js
new file mode 100644
index 00000000..7d3d27e2
--- /dev/null
+++ b/src/components/chat_message/chat_message.js
@@ -0,0 +1,62 @@
+import { mapState, mapGetters } from 'vuex'
+import Attachment from '../attachment/attachment.vue'
+import UserAvatar from '../user_avatar/user_avatar.vue'
+import Gallery from '../gallery/gallery.vue'
+import LinkPreview from '../link-preview/link-preview.vue'
+import StatusContent from '../status_content/status_content.vue'
+import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
+
+const ChatMessage = {
+ name: 'ChatMessage',
+ props: [
+ 'edited',
+ 'noHeading',
+ 'chatViewItem',
+ 'sequenceHovered'
+ ],
+ components: {
+ Attachment,
+ StatusContent,
+ UserAvatar,
+ Gallery,
+ LinkPreview,
+ ChatMessageDate
+ },
+ computed: {
+ // Returns HH:MM (hours and minutes) in local time.
+ createdAt () {
+ const time = this.chatViewItem.data.created_at
+ const lang = this.mergedConfig.interfaceLanguage
+ return time.toLocaleTimeString(lang, { hour: '2-digit', minute: '2-digit', hour12: false })
+ },
+ isCurrentUser () {
+ return this.message.account_id === this.currentUser.id
+ },
+ message () {
+ return this.chatViewItem.data
+ },
+ isMessage () {
+ return this.chatViewItem.type === 'message'
+ },
+ messageForStatusContent () {
+ return {
+ summary: '',
+ statusnet_html: this.message.content,
+ text: this.message.content,
+ attachments: []
+ }
+ },
+ ...mapState({
+ betterShadow: state => state.interface.browserSupport.cssFilter,
+ currentUser: state => state.users.currentUser
+ }),
+ ...mapGetters(['mergedConfig'])
+ },
+ methods: {
+ onHover (bool) {
+ this.$emit('hover', { state: bool, sequenceId: this.chatViewItem.sequenceId })
+ }
+ }
+}
+
+export default ChatMessage
diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss
new file mode 100644
index 00000000..1859dbc9
--- /dev/null
+++ b/src/components/chat_message/chat_message.scss
@@ -0,0 +1,171 @@
+@import '../../_variables.scss';
+
+.direct-conversation-status-wrapper {
+ &.sequence-hovered {
+ .animated.avatar {
+ canvas {
+ display: none;
+ }
+ img {
+ visibility: visible;
+ }
+ }
+ }
+
+ &:last-child {
+ margin-bottom: 16px;
+ }
+
+ .media-heading {
+ margin-left: 43px;
+ margin-bottom: 4px;
+ }
+
+ .status {
+ padding: 0.75em;
+ }
+
+ .direct-conversation {
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ word-break: break-word;
+ border-left-width: 0px;
+ min-width: 0;
+ display: flex;
+ width: 100%;
+ padding-bottom: 7px;
+
+ .avatar-wrapper {
+ margin-right: 10px;
+ margin-top: auto;
+ width: 32px;
+ }
+
+ .link-preview, .attachments {
+ margin-bottom: 0.9em;
+ }
+
+ .status-content {
+ line-height: 1.4em;
+ }
+
+ .direct-conversation-inner {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ max-width: 80%;
+ position: relative;
+ float: right;
+ min-width: 10rem;
+
+ .trigger {
+ width: 100%;
+ }
+
+ .popover {
+ width: 12rem;
+
+ .tooltip-arrow.popover-arrow {
+ // overrides inline html arrow position
+ left: calc(100% - 30px) !important;
+ }
+ }
+
+ .poll {
+ margin-bottom: 1em;
+ }
+
+ &.with-media {
+ width: 100%;
+
+ .gallery-row {
+ overflow: hidden;
+ }
+
+ .status {
+ width: 100%;
+ }
+ }
+
+ .status {
+ box-sizing: border-box;
+ border-radius: $fallback--chatMessageRadius;
+ border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
+ }
+
+ .created-at {
+ float: right;
+ font-size: 0.8em;
+ margin: -10px 0 -5px 4px;
+ position: relative;
+ font-style: italic;
+ opacity: 0.8;
+ a {
+ color: var(--chatMessageOutgoingText, $fallback--text);
+ }
+
+ ::before {
+ font-size: 1em;
+ }
+ }
+
+ .status-content {
+ white-space: normal;
+
+ &::after {
+ margin-right: 75px;
+ content: " ";
+ display: inline-block;
+ }
+ }
+ }
+
+ &.incoming {
+ .status {
+ background-color: var(--chatMessageIncomingBg, $fallback--bg);
+ border: 1px solid var(--chatMessageIncomingBorder, --border);
+ color: var(--chatMessageIncomingText, $fallback--text);
+
+ a {
+ color: var(--chatMessageIncomingLink, $fallback--link);
+ }
+ }
+ .created-at {
+ a {
+ color: var(--chatMessageIncomingText, $fallback--text);
+ }
+ }
+ }
+
+ &.outgoing {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-content: end;
+ justify-content: flex-end;
+
+ a {
+ color: var(--chatMessageOutgoingLink, $fallback--link);
+ }
+
+ .status {
+ border: 1px solid var(--chatMessageOutgoingBorder, --lightBg);
+ color: var(--chatMessageOutgoingText, $fallback--text);
+ background-color: var(--chatMessageOutgoingBg, $fallback--lightBg);
+ }
+
+ .direct-conversation-inner {
+ align-items: flex-end;
+ }
+ }
+ }
+}
+
+.date-separator {
+ text-align: center;
+ margin: 1.4em 0;
+ font-size: 0.9em;
+ user-select: none;
+ color: $fallback--text;
+ color: var(--faintedText, $fallback--text);
+}
diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue
new file mode 100644
index 00000000..79b775cc
--- /dev/null
+++ b/src/components/chat_message/chat_message.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+ {{ createdAt }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/chat_message_date/chat_message_date.vue b/src/components/chat_message_date/chat_message_date.vue
new file mode 100644
index 00000000..24060f41
--- /dev/null
+++ b/src/components/chat_message_date/chat_message_date.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
diff --git a/src/components/chat_new/chat_new.js b/src/components/chat_new/chat_new.js
new file mode 100644
index 00000000..c0571542
--- /dev/null
+++ b/src/components/chat_new/chat_new.js
@@ -0,0 +1,74 @@
+import { throttle } from 'lodash'
+import { mapState, mapGetters } from 'vuex'
+import BasicUserCard from '../basic_user_card/basic_user_card.vue'
+import UserAvatar from '../user_avatar/user_avatar.vue'
+
+const chatNew = {
+ components: {
+ BasicUserCard,
+ UserAvatar
+ },
+ data () {
+ return {
+ suggestions: [],
+ userIds: [],
+ loading: false,
+ query: ''
+ }
+ },
+ async created () {
+ const { chats } = await this.backendInteractor.chats()
+ chats.forEach(chat => this.suggestions.push(chat.account))
+ },
+ computed: {
+ users () {
+ return this.userIds.map(userId => this.findUser(userId))
+ },
+ availableUsers () {
+ if (this.query.length !== 0) {
+ return this.users
+ } else {
+ return this.suggestions
+ }
+ },
+ ...mapState({
+ currentUser: state => state.users.currentUser,
+ backendInteractor: state => state.api.backendInteractor
+ }),
+ ...mapGetters(['findUser'])
+ },
+ methods: {
+ goBack () {
+ this.$emit('cancel')
+ },
+ goToNewChat (user) {
+ this.$router.push({ name: 'chat', params: { recipient_id: user.id } })
+ },
+ onInput () {
+ this.search(this.query)
+ },
+ addUser (user) {
+ this.selectedUserIds.push(user.id)
+ this.query = ''
+ },
+ removeUser (userId) {
+ this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId)
+ },
+ search: throttle(function (query) {
+ if (!query) {
+ this.loading = false
+ return
+ }
+
+ this.loading = true
+ this.userIds = []
+ this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts' })
+ .then(data => {
+ this.loading = false
+ this.userIds = data.accounts.map(a => a.id)
+ })
+ })
+ }
+}
+
+export default chatNew
diff --git a/src/components/chat_new/chat_new.scss b/src/components/chat_new/chat_new.scss
new file mode 100644
index 00000000..780e307b
--- /dev/null
+++ b/src/components/chat_new/chat_new.scss
@@ -0,0 +1,87 @@
+.direct-conversation-new {
+ .panel-heading {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ .btn {
+ padding-left: 1em;
+ padding-right: 1em;
+ }
+
+ .selected-user-list {
+ padding-left: 0.7em;
+ padding-top: 0.5em;
+
+ .selected-user {
+ cursor: pointer;
+ display: inline-block;
+ border: 1px solid;
+ border-color: $fallback--link;
+ border-color: var(--link, $fallback--link);
+ color: $fallback--link;
+ color: var(--link, $fallback--link);
+ padding: 0.5em;
+ border-radius: $fallback--attachmentRadius;
+ border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
+ margin-right: 0.5em;
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+
+ &:hover {
+ background-color: var(--selectedPost, $fallback--lightBg);
+ }
+ }
+
+ .notice-dismissible {
+ padding-right: 2.3rem;
+ position: relative;
+
+ .dismiss {
+ position: absolute;
+ top: 0;
+ right: 0;
+ padding: .35em;
+ color: inherit;
+
+ i {
+ color: $fallback--link;
+ color: var(--link, $fallback--link);
+ }
+ }
+ }
+ }
+
+ .member-list {
+ padding-bottom: 0.67rem;
+
+ .user-card-wrap {
+ .basic-user-card {
+ &:hover {
+ cursor: pointer;
+ background-color: var(--selectedPost, $fallback--lightBg);
+ }
+ }
+ }
+ }
+
+ .input-wrap {
+ display: flex;
+ margin: 0.7em 0.5em 0.7em 0.5em;
+
+ .button-icon {
+ font-size: 1.5em;
+ float: right;
+ margin-right: 0.3em;
+ }
+
+ .btn {
+ margin: 0 0.7em;
+ width: 6em;
+ }
+
+ input {
+ width: 100%;
+ }
+ }
+}
diff --git a/src/components/chat_new/chat_new.vue b/src/components/chat_new/chat_new.vue
new file mode 100644
index 00000000..9d8839ac
--- /dev/null
+++ b/src/components/chat_new/chat_new.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
diff --git a/src/components/chat_title/chat_title.js b/src/components/chat_title/chat_title.js
new file mode 100644
index 00000000..07590d58
--- /dev/null
+++ b/src/components/chat_title/chat_title.js
@@ -0,0 +1,39 @@
+import Vue from 'vue'
+import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
+import { mapState } from 'vuex'
+
+const USER_LIMIT = 10
+
+export default Vue.component('direct-conversation-title', {
+ name: 'ChatTitle',
+ props: [
+ 'users', 'fallbackUser'
+ ],
+ computed: {
+ ...mapState({
+ currentUser: state => state.users.currentUser
+ }),
+ otherUsersTruncated () {
+ return this.otherUsers.slice(0, USER_LIMIT)
+ },
+ otherUsers () {
+ let otherUsers = this.users.filter(recipient => recipient.id !== this.currentUser.id)
+ if (otherUsers.length === 0) {
+ return [this.fallbackUser]
+ } else {
+ return otherUsers
+ }
+ },
+ restCount () {
+ return this.otherUsers.length - USER_LIMIT
+ },
+ title () {
+ return this.otherUsers.map(u => u.screen_name).join(', ')
+ }
+ },
+ 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
new file mode 100644
index 00000000..b8bbf171
--- /dev/null
+++ b/src/components/chat_title/chat_title.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.js b/src/components/mobile_post_status_button/mobile_post_status_button.js
index 0ad12bb1..6348277b 100644
--- a/src/components/mobile_post_status_button/mobile_post_status_button.js
+++ b/src/components/mobile_post_status_button/mobile_post_status_button.js
@@ -1,5 +1,10 @@
import { debounce } from 'lodash'
+const HIDDEN_FOR_PAGES = new Set([
+ 'chats',
+ 'chat'
+])
+
const MobilePostStatusButton = {
data () {
return {
@@ -27,6 +32,8 @@ const MobilePostStatusButton = {
return !!this.$store.state.users.currentUser
},
isHidden () {
+ if (HIDDEN_FOR_PAGES.has(this.$route.name)) { return true }
+
return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
},
autohideFloatingPostButton () {
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index 8cd04dc7..1543c15c 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -17,6 +17,11 @@
{{ $t("nav.dms") }}
+
+
+ {{ $t("nav.chats") }}
+
+
{{ $t("nav.friend_requests") }}
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index 1cf4c9bc..246e698d 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -2,6 +2,7 @@ import Status from '../status/status.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue'
import Timeago from '../timeago/timeago.vue'
+import StatusContent from '../status_content/status_content.vue'
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@@ -19,7 +20,8 @@ const Notification = {
Status,
UserAvatar,
UserCard,
- Timeago
+ Timeago,
+ StatusContent
},
methods: {
toggleUserExpanded () {
@@ -79,6 +81,15 @@ const Notification = {
},
isStatusNotification () {
return isStatusNotification(this.notification.type)
+ },
+ // TODO:
+ messageForStatusContent () {
+ return {
+ summary: '',
+ statusnet_html: this.notification.chatMessage.content,
+ text: this.notification.chatMessage.content,
+ attachments: []
+ }
}
}
}
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 0e46a2a7..3a8eaf26 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -89,6 +89,9 @@
+
+
+
+
+
+
+
+
+
+
+
= 2
+ this.$store.state.instance.pollLimits.max_options >= 2 &&
+ this.disablePolls !== true
},
hideScopeNotice () {
- return this.$store.getters.mergedConfig.hideScopeNotice
+ return this.disableNotice || this.$store.getters.mergedConfig.hideScopeNotice
},
pollContentError () {
return this.pollFormVisible &&
@@ -182,7 +192,8 @@ const PostStatusForm = {
}
this.posting = true
- statusPoster.postStatus({
+
+ const postingOptions = {
status: newStatus.status,
spoilerText: newStatus.spoilerText || null,
visibility: newStatus.visibility,
@@ -192,7 +203,11 @@ const PostStatusForm = {
inReplyToStatusId: this.replyTo,
contentType: newStatus.contentType,
poll
- }).then((data) => {
+ }
+
+ const poster = this.poster ? this.poster : statusPoster.postStatus
+
+ poster(postingOptions).then((data) => {
if (!data.error) {
this.newStatus = {
status: '',
@@ -205,7 +220,12 @@ const PostStatusForm = {
this.pollFormVisible = false
this.$refs.mediaUpload.clearFile()
this.clearPollForm()
- this.$emit('posted')
+ this.$emit('posted', data)
+ if (this.preserveFocus) {
+ this.$nextTick(() => {
+ this.$refs.textarea.focus()
+ })
+ }
let el = this.$el.querySelector('textarea')
el.style.height = 'auto'
el.style.height = undefined
@@ -270,6 +290,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.$refs['emoji-input'].resize()
return
}
@@ -322,8 +343,10 @@ const PostStatusForm = {
// BEGIN content size update
target.style.height = 'auto'
- const newHeight = target.scrollHeight - vertPadding
+ const heightWithoutPadding = target.scrollHeight - vertPadding
+ const newHeight = this.maxHeight ? Math.min(heightWithoutPadding, this.maxHeight) : heightWithoutPadding
target.style.height = `${newHeight}px`
+ this.$emit('resize', newHeight)
// END content size update
// We check where the bottom border of form-bottom element is, this uses findOffset
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 9789a481..bba79989 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -62,14 +62,13 @@
{{ $t('post_status.direct_warning_to_all') }}
-
+
({
+ data: [],
+ pagination: { maxId: undefined, minId: undefined },
+ idStore: {}
+})
+
+const defaultState = {
+ chatList: emptyChatList(),
+ openedChats: {},
+ openedChatMessageServices: {},
+ fetcher: undefined,
+ chatFocused: false,
+ currentChatId: null
+}
+
+const chats = {
+ state: { ...defaultState },
+ getters: {
+ currentChat: state => state.openedChats[state.currentChatId],
+ currentChatMessageService: state => state.openedChatMessageServices[state.currentChatId]
+ },
+ actions: {
+ // Chat list
+ startFetchingChats ({ dispatch }) {
+ setInterval(() => {
+ dispatch('fetchChats', { reset: true })
+ // dispatch('refreshCurrentUser')
+ }, 5000)
+ },
+ fetchChats ({ dispatch, rootState, commit }, params = {}) {
+ const pagination = rootState.chats.chatList.pagination
+ const opts = { maxId: params.reset ? undefined : pagination.maxId }
+
+ return rootState.api.backendInteractor.chats(opts)
+ .then(({ chats, pagination }) => {
+ dispatch('addNewChats', { chats, pagination })
+ return chats
+ })
+ },
+ addNewChats ({ rootState, commit, dispatch, rootGetters }, { chats, pagination }) {
+ commit('addNewChats', { dispatch, chats, pagination, rootGetters })
+ },
+ updateChatByAccountId: debounce(({ rootState, commit, dispatch, rootGetters }, { accountId }) => {
+ rootState.api.backendInteractor.getOrCreateChat({ accountId }).then(chat => {
+ commit('updateChat', { dispatch, rootGetters, chat: parseChat(chat) })
+ })
+ }, 100),
+
+ // Opened Chats
+ startFetchingCurrentChat ({ commit, dispatch }, { fetcher }) {
+ dispatch('setCurrentChatFetcher', { fetcher })
+ },
+ setCurrentChatFetcher ({ rootState, commit }, { fetcher }) {
+ commit('setCurrentChatFetcher', { fetcher })
+ },
+ addOpenedChat ({ commit, dispatch }, { chat }) {
+ commit('addOpenedChat', { dispatch, chat: parseChat(chat) })
+ dispatch('addNewUsers', [chat.account])
+ },
+ addChatMessages ({ commit }, value) {
+ commit('addChatMessages', value)
+ },
+ setChatFocused ({ commit }, value) {
+ commit('setChatFocused', value)
+ },
+ resetChatNewMessageCount ({ commit }, value) {
+ commit('resetChatNewMessageCount', value)
+ },
+ removeFromCurrentChatStatuses ({ commit }, { id }) {
+ commit('removeFromCurrentChatStatuses', id)
+ },
+ clearCurrentChat ({ rootState, commit, dispatch }, value) {
+ commit('setCurrentChatId', { chatId: undefined })
+ commit('setCurrentChatFetcher', { fetcher: undefined })
+ },
+ readChat ({ rootState, dispatch }, { id }) {
+ dispatch('resetChatNewMessageCount')
+ rootState.api.backendInteractor.readChat({ id }).then(() => {
+ // dispatch('refreshCurrentUser')
+ })
+ }
+ },
+ mutations: {
+ setCurrentChatFetcher (state, { fetcher }) {
+ let prevFetcher = state.fetcher
+ if (prevFetcher) {
+ clearInterval(prevFetcher)
+ }
+ state.fetcher = fetcher && fetcher()
+ },
+ addOpenedChat (state, { _dispatch, chat }) {
+ state.currentChatId = chat.id
+ state.openedChats[chat.id] = chat
+
+ if (!state.openedChatMessageServices[chat.id]) {
+ state.openedChatMessageServices[chat.id] = chatService.empty(chat.id)
+ }
+ },
+ setCurrentChatId (state, { chatId }) {
+ state.currentChatId = chatId
+ },
+ addNewChats (state, { _dispatch, chats, pagination, _rootGetters }) {
+ if (chats.length > 0) {
+ state.chatList.pagination = { maxId: last(chats).id }
+ }
+ chats.forEach((conversation) => {
+ // This is to prevent duplicate conversations being added
+ // (right now, backend can return the same conversation on different pages)
+ if (!state.chatList.idStore[conversation.id]) {
+ state.chatList.data.push(conversation)
+ state.chatList.idStore[conversation.id] = conversation
+ } else {
+ const chat = find(state.chatList.data, { id: conversation.id })
+ chat.last_status = conversation.last_status
+ chat.unread = conversation.unread
+ state.chatList.idStore[conversation.id] = conversation
+ }
+ })
+ },
+ updateChat (state, { _dispatch, chat: updatedChat, _rootGetters }) {
+ let chat = find(state.chatList.data, { id: updatedChat.id })
+ if (chat) {
+ state.chatList.data = state.chatList.data.filter(d => {
+ return d.id !== updatedChat.id
+ })
+ }
+ state.chatList.data.unshift(updatedChat)
+ state.chatList.idStore[updatedChat.id] = updatedChat
+ },
+ deleteChat (state, { _dispatch, id, _rootGetters }) {
+ state.chats.data = state.chats.data.filter(conversation =>
+ conversation.last_status.id !== id
+ )
+ state.chats.idStore = omitBy(state.chats.idStore, conversation => conversation.last_status.id === id)
+ },
+ resetChats (state, { _dispatch }) {
+ state.chats.data = []
+ state.chats.idStore = {}
+ },
+ setChatsLoading (state, { value }) {
+ state.chats.loading = value
+ },
+ addChatMessages (state, { chatId, messages }) {
+ const chatMessageService = state.openedChatMessageServices[chatId]
+ if (chatMessageService) {
+ chatService.add(chatMessageService, { messages: messages.map(parseChatMessage) })
+ }
+ },
+ resetChatNewMessageCount (state, _value) {
+ const chatMessageService = state.openedChatMessageServices[state.currentChatId]
+ chatService.resetNewMessageCount(chatMessageService)
+ },
+ setChatFocused (state, value) {
+ state.chatFocused = value
+ }
+ }
+}
+
+export default chats
diff --git a/src/modules/config.js b/src/modules/config.js
index 8f4638f5..024f257b 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -35,7 +35,8 @@ export const defaultState = {
repeats: true,
moves: true,
emojiReactions: false,
- followRequest: true
+ followRequest: true,
+ chatMention: true
},
webPushNotifications: false,
muteWords: [],
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index cd8c1dba..ed78cffd 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -83,7 +83,8 @@ const visibleNotificationTypes = (rootState) => {
rootState.config.notificationVisibility.repeats && 'repeat',
rootState.config.notificationVisibility.follows && 'follow',
rootState.config.notificationVisibility.moves && 'move',
- rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reactions'
+ rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reactions',
+ rootState.config.notificationVisibility.chatMention && 'pleroma:chat_mention'
].filter(_ => _)
}
@@ -331,6 +332,11 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
dispatch('fetchEmojiReactionsBy', notification.status.id)
}
+ if (notification.type === 'pleroma:chat_mention') {
+ dispatch('addChatMessages', { chatId: notification.chatMessage.chat_id, messages: [notification.chatMessage] })
+ dispatch('updateChatByAccountId', { accountId: notification.from_profile.id })
+ }
+
// Only add a new notification if we don't have one for the same action
if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
state.notifications.maxId = notification.id > state.notifications.maxId
@@ -373,6 +379,8 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
} else if (isStatusNotification(notification.type)) {
notifObj.body = notification.status.text
+ } else if (notification.type === 'pleroma:chat_mention') {
+ notifObj.body = notification.chatMessage.content
}
// Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
@@ -489,7 +497,7 @@ export const mutations = {
},
setDeleted (state, { status }) {
const newStatus = state.allStatusesObject[status.id]
- newStatus.deleted = true
+ if (newStatus) newStatus.deleted = true
},
setManyDeleted (state, condition) {
Object.values(state.allStatusesObject).forEach(status => {
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 9c7530a2..04c8f214 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,5 +1,5 @@
import { each, map, concat, last, get } from 'lodash'
-import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
+import { parseStatus, parseUser, parseNotification, parseAttachment, parseChat } from '../entity_normalizer/entity_normalizer.service.js'
import 'whatwg-fetch'
import { RegistrationError, StatusCodeError } from '../errors/errors'
@@ -78,6 +78,10 @@ const MASTODON_STREAMING = '/api/v1/streaming'
const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
+const PLEROMA_CHATS_URL = `/api/v1/pleroma/chats`
+const PLEROMA_CHAT_URL = id => `/api/v1/pleroma/chats/by-account-id/${id}`
+const PLEROMA_CHAT_MESSAGES_URL = id => `/api/v1/pleroma/chats/${id}/messages`
+const PLEROMA_CHAT_READ_URL = id => `/api/v1/pleroma/chats/${id}/read`
const oldfetch = window.fetch
@@ -1119,6 +1123,60 @@ export const handleMastoWS = (wsEvent) => {
}
}
+const chats = ({ maxId, sinceId, limit = 20, recipients = [], credentials }) => {
+ let url = PLEROMA_CHATS_URL
+ const recipientIds = recipients.map(r => `recipients[]=${r}`).join('&')
+ const args = [
+ maxId && `max_id=${maxId}`,
+ sinceId && `since_id=${sinceId}`,
+ limit && `limit=${limit}`,
+ recipientIds && `${recipientIds}`
+ ].filter(_ => _).join('&')
+
+ let pagination = {}
+ url = url + (args ? '?' + args : '')
+ return fetch(url, { headers: authHeaders(credentials) })
+ .then((data) => data.json())
+ .then((data) => {
+ return { chats: data.map(parseChat), pagination }
+ })
+}
+
+const getOrCreateChat = ({ accountId, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_CHAT_URL(accountId),
+ method: 'POST',
+ credentials
+ })
+}
+
+const chatMessages = ({ id, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_CHAT_MESSAGES_URL(id),
+ method: 'GET',
+ credentials
+ })
+}
+
+const postChatMessage = ({ id, content, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_CHAT_MESSAGES_URL(id),
+ method: 'POST',
+ payload: {
+ 'content': content
+ },
+ credentials
+ })
+}
+
+const readChat = ({ id, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_CHAT_READ_URL(id),
+ method: 'POST',
+ credentials
+ })
+}
+
const apiService = {
verifyCredentials,
fetchTimeline,
@@ -1195,7 +1253,12 @@ const apiService = {
searchUsers,
fetchDomainMutes,
muteDomain,
- unmuteDomain
+ unmuteDomain,
+ chats,
+ getOrCreateChat,
+ chatMessages,
+ postChatMessage,
+ readChat
}
export default apiService
diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js
new file mode 100644
index 00000000..b570a065
--- /dev/null
+++ b/src/services/chat_service/chat_service.js
@@ -0,0 +1,100 @@
+import _ from 'lodash'
+
+const empty = (chatId) => {
+ return {
+ idIndex: {},
+ messages: [],
+ newMessageCount: 0,
+ lastSeenTimestamp: 0,
+ chatId: chatId
+ }
+}
+
+const add = (storage, { messages: newMessages }) => {
+ if (!storage) { return }
+ for (let i = 0; i < newMessages.length; i++) {
+ let message = newMessages[i]
+
+ // sanity check
+ if (message.chat_id !== storage.chatId) { return }
+
+ if (!storage.idIndex[message.id]) {
+ if (storage.lastSeenTimestamp < message.created_at) {
+ storage.newMessageCount++
+ }
+ storage.messages.push(message)
+ storage.idIndex[message.id] = message
+ }
+ }
+}
+
+const resetNewMessageCount = (storage) => {
+ if (!storage) { return }
+ storage.newMessageCount = 0
+ storage.lastSeenTimestamp = new Date()
+}
+
+// Inserts date separators and marks the head and tail if it's the sequence of messages made by the same user
+const getView = (storage) => {
+ if (!storage) { return [] }
+ let messages = _.sortBy(storage.messages, 'id')
+
+ let res = []
+
+ let prev = messages[messages.length - 1]
+ let currentSequenceId
+
+ let firstMessages = messages[0]
+
+ if (firstMessages) {
+ let date = new Date(firstMessages.created_at)
+ date.setHours(0, 0, 0, 0)
+ res.push({ type: 'date', date: date, id: date.getTime().toString() })
+ }
+
+ let afterDate = false
+
+ for (let i = 0; i < messages.length; i++) {
+ let message = messages[i]
+ let nextMessage = messages[i + 1]
+
+ let date = new Date(message.created_at)
+ date.setHours(0, 0, 0, 0)
+
+ // insert date separator and start a new sequence
+ if (prev && prev.date < date) {
+ res.push({ type: 'date', date: date, id: date.getTime().toString() })
+ prev['isTail'] = true
+ currentSequenceId = undefined
+ afterDate = true
+ }
+
+ let object = { type: 'message', data: message, date: date, id: message.id, sequenceId: currentSequenceId }
+
+ // end a message sequence
+ if ((nextMessage && nextMessage.account_id) !== message.account_id) {
+ object['isTail'] = true
+ currentSequenceId = undefined
+ }
+ // start a new message sequence
+ if ((prev && prev.data && prev.data.account_id) !== message.account_id || afterDate) {
+ currentSequenceId = _.uniqueId()
+ object['isHead'] = true
+ object['sequenceId'] = currentSequenceId
+ }
+ res.push(object)
+ prev = object
+ afterDate = false
+ }
+
+ return res
+}
+
+const ChatService = {
+ add,
+ empty,
+ getView,
+ resetNewMessageCount
+}
+
+export default ChatService
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 6dac7c15..20e7a616 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -342,6 +342,7 @@ export const parseNotification = (data) => {
output.type = mastoDict[data.type] || data.type
output.seen = data.pleroma.is_seen
output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
+ output.chatMessage = output.type === 'pleroma:chat_mention' ? parseChatMessage(data.chat_message) : null
output.action = output.status // TODO: Refactor, this is unneeded
output.target = output.type !== 'move'
? null
@@ -356,7 +357,7 @@ export const parseNotification = (data) => {
? parseStatus(data.notice.favorited_status)
: parsedNotice
output.action = parsedNotice
- output.from_profile = parseUser(data.from_profile)
+ output.from_profile = output.type === 'pleroma:chat_mention' ? parseUser(data.account) : parseUser(data.from_profile)
}
output.created_at = new Date(data.created_at)
@@ -369,3 +370,19 @@ const isNsfw = (status) => {
const nsfwRegex = /#nsfw/i
return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex)
}
+
+export const parseChat = (chat) => {
+ let output = chat
+ output.id = parseInt(chat.id, 10)
+ output.account = parseUser(chat.account)
+ output.unread = chat.unread
+ output.lastMessage = undefined
+ return output
+}
+
+export const parseChatMessage = (message) => {
+ let output = message
+ output.created_at = new Date(message.created_at)
+ output.chat_id = parseInt(message.chat_id, 10)
+ return output
+}
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index eb479227..d73f747c 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -9,7 +9,8 @@ export const visibleTypes = store => ([
store.state.config.notificationVisibility.follows && 'follow',
store.state.config.notificationVisibility.followRequest && 'follow_request',
store.state.config.notificationVisibility.moves && 'move',
- store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
+ store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction',
+ store.state.config.notificationVisibility.chatMention && 'pleroma:chat_mention'
].filter(_ => _))
const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction']
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index fbdcf562..07425abd 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -106,7 +106,8 @@ export const generateRadii = (input) => {
avatar: 5,
avatarAlt: 50,
tooltip: 2,
- attachment: 5
+ attachment: 5,
+ chatMessage: inputRadii.panel
})
return {
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index 0c1fe543..7f0f6078 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -627,5 +627,51 @@ export const SLOT_INHERITANCE = {
layer: 'badge',
variant: 'badgeNotification',
textColor: 'bw'
+ },
+
+ chatBg: {
+ depends: ['bg'],
+ layer: 'bg'
+ },
+
+ chatMessageIncomingBg: {
+ depends: ['bg'],
+ layer: 'bg'
+ },
+
+ chatMessageIncomingText: {
+ depends: ['text'],
+ layer: 'text'
+ },
+
+ chatMessageIncomingLink: {
+ depends: ['link'],
+ layer: 'link'
+ },
+
+ chatMessageIncomingBorder: {
+ depends: ['fg'],
+ opacity: 'border',
+ color: (mod, fg) => brightness(2 * mod, fg).rgb
+ },
+
+ chatMessageOutgoingBg: {
+ depends: ['bg'],
+ color: (mod, fg) => brightness(5 * mod, fg).rgb
+ },
+
+ chatMessageOutgoingText: {
+ depends: ['text'],
+ layer: 'text'
+ },
+
+ chatMessageOutgoingLink: {
+ depends: ['link'],
+ layer: 'link'
+ },
+
+ chatMessageOutgoingBorder: {
+ depends: ['bg'],
+ opacity: 'bg'
}
}
diff --git a/static/fontello.json b/static/fontello.json
old mode 100755
new mode 100644
diff --git a/static/themes/breezy-dark.json b/static/themes/breezy-dark.json
index 76b962c5..6fb9d09c 100644
--- a/static/themes/breezy-dark.json
+++ b/static/themes/breezy-dark.json
@@ -115,7 +115,8 @@
"cOrange": "#f67400",
"btnPressed": "--accent",
"selectedMenu": "--accent",
- "selectedMenuPopover": "--accent"
+ "selectedMenuPopover": "--accent",
+ "chatMessageIncomingBorder": "#3d4349"
},
"radii": {
"btn": "2",
diff --git a/static/themes/redmond-xx-se.json b/static/themes/redmond-xx-se.json
index 7a4a29da..22d0d184 100644
--- a/static/themes/redmond-xx-se.json
+++ b/static/themes/redmond-xx-se.json
@@ -286,7 +286,8 @@
"cGreen": "#008000",
"cOrange": "#808000",
"highlight": "--accent",
- "selectedPost": "--bg,-10"
+ "selectedPost": "--bg,-10",
+ "chatFg": "#808080"
},
"radii": {
"btn": "0",
diff --git a/static/themes/redmond-xx.json b/static/themes/redmond-xx.json
index ff95b1e0..33544cbe 100644
--- a/static/themes/redmond-xx.json
+++ b/static/themes/redmond-xx.json
@@ -277,7 +277,8 @@
"cGreen": "#008000",
"cOrange": "#808000",
"highlight": "--accent",
- "selectedPost": "--bg,-10"
+ "selectedPost": "--bg,-10",
+ "chatMessageIncomingBorder": "#808080"
},
"radii": {
"btn": "0",
diff --git a/static/themes/redmond-xxi.json b/static/themes/redmond-xxi.json
index f788bdb8..0bff9fe1 100644
--- a/static/themes/redmond-xxi.json
+++ b/static/themes/redmond-xxi.json
@@ -259,7 +259,8 @@
"cGreen": "#669966",
"cOrange": "#cc6633",
"highlight": "--accent",
- "selectedPost": "--bg,-10"
+ "selectedPost": "--bg,-10",
+ "chatFg": "#808080"
},
"radii": {
"btn": "0",
From f06ec18cd7078a0aab8163d7c3a825589132d460 Mon Sep 17 00:00:00 2001
From: eugenijm
Date: Thu, 7 May 2020 19:31:42 +0300
Subject: [PATCH 02/41] Chat fixes
---
src/components/nav_panel/nav_panel.vue | 2 +-
src/components/post_status_form/post_status_form.js | 2 +-
src/services/entity_normalizer/entity_normalizer.service.js | 1 +
3 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index 1543c15c..5c35cf31 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -17,7 +17,7 @@
{{ $t("nav.dms") }}
-
+
{{ $t("nav.chats") }}
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index ea05396a..e854454d 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -218,7 +218,7 @@ const PostStatusForm = {
poll: {}
}
this.pollFormVisible = false
- this.$refs.mediaUpload.clearFile()
+ this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile()
this.clearPollForm()
this.$emit('posted', data)
if (this.preserveFocus) {
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 20e7a616..3d321d65 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -382,6 +382,7 @@ export const parseChat = (chat) => {
export const parseChatMessage = (message) => {
let output = message
+ output.id = parseInt(message.id, 10)
output.created_at = new Date(message.created_at)
output.chat_id = parseInt(message.chat_id, 10)
return output
From 5e93ca3edb4a7866c662418c49af17daba91baa2 Mon Sep 17 00:00:00 2001
From: eugenijm
Date: Thu, 7 May 2020 21:50:30 +0300
Subject: [PATCH 03/41] WIP chat avatars
---
src/boot/routes.js | 2 +-
src/components/chat/chat.js | 2 +-
src/components/chat/chat.vue | 1 +
src/components/chat_message/chat_message.js | 12 ++++++++++--
src/components/chat_message/chat_message.scss | 1 -
src/components/chat_message/chat_message.vue | 15 +++++++++++++++
src/components/notification/notification.js | 6 +++++-
src/components/notification/notification.vue | 6 ++----
.../post_status_form/post_status_form.js | 5 +++--
src/components/status_content/status_content.js | 2 +-
src/components/status_content/status_content.vue | 2 +-
11 files changed, 40 insertions(+), 14 deletions(-)
diff --git a/src/boot/routes.js b/src/boot/routes.js
index 48b8c9da..c770746f 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -58,7 +58,7 @@ export default (store) => {
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
- { name: 'chat', path: '/chats/:recipient_id', component: Chat, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute },
+ { name: 'chat', path: '/users/:username/chats/:recipient_id', component: Chat, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute },
{ name: 'chats', path: '/users/:username/chats', component: ChatList, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute },
{ name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration },
diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js
index 2e3fe975..8f8e279d 100644
--- a/src/components/chat/chat.js
+++ b/src/components/chat/chat.js
@@ -282,7 +282,7 @@ const Chat = {
}
}, 100),
goBack () {
- this.$router.push({ name: 'chats', params: { username: this.screen_name } })
+ this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } })
},
fetchChat (isFirstFetch, chatId) {
this.chatViewItems = chatService.getView(this.currentChatMessageService)
diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue
index 02b03609..5847348e 100644
--- a/src/components/chat/chat.vue
+++ b/src/components/chat/chat.vue
@@ -71,6 +71,7 @@
:poster="poster"
:preserve-focus="true"
:polls-available="false"
+ :auto-focus="true"
:placeholder="formPlaceholder"
max-height="160"
@resize="handleResize"
diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js
index 7d3d27e2..7e3cfad3 100644
--- a/src/components/chat_message/chat_message.js
+++ b/src/components/chat_message/chat_message.js
@@ -5,6 +5,7 @@ import Gallery from '../gallery/gallery.vue'
import LinkPreview from '../link-preview/link-preview.vue'
import StatusContent from '../status_content/status_content.vue'
import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
+import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const ChatMessage = {
name: 'ChatMessage',
@@ -32,9 +33,15 @@ const ChatMessage = {
isCurrentUser () {
return this.message.account_id === this.currentUser.id
},
+ author () {
+ return this.findUser(this.message.account_id)
+ },
message () {
return this.chatViewItem.data
},
+ userProfileLink () {
+ return generateProfileLink(this.author.id, this.author.screen_name, this.$store.state.instance.restrictedNicknames)
+ },
isMessage () {
return this.chatViewItem.type === 'message'
},
@@ -48,9 +55,10 @@ const ChatMessage = {
},
...mapState({
betterShadow: state => state.interface.browserSupport.cssFilter,
- currentUser: state => state.users.currentUser
+ currentUser: state => state.users.currentUser,
+ restrictedNicknames: state => state.instance.restrictedNicknames
}),
- ...mapGetters(['mergedConfig'])
+ ...mapGetters(['mergedConfig', 'findUser'])
},
methods: {
onHover (bool) {
diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss
index 1859dbc9..3e219e24 100644
--- a/src/components/chat_message/chat_message.scss
+++ b/src/components/chat_message/chat_message.scss
@@ -37,7 +37,6 @@
.avatar-wrapper {
margin-right: 10px;
- margin-top: auto;
width: 32px;
}
diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue
index 79b775cc..7aeec30b 100644
--- a/src/components/chat_message/chat_message.vue
+++ b/src/components/chat_message/chat_message.vue
@@ -10,6 +10,21 @@
class="direct-conversation"
:class="[{ 'outgoing': isCurrentUser, 'incoming': !isCurrentUser }]"
>
+
+
+
+
+
-
+
+
+
+
Date: Sun, 10 May 2020 21:27:32 +0300
Subject: [PATCH 05/41] wip
---
src/components/chat/chat.vue | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue
index dd56c03b..2aad5a49 100644
--- a/src/components/chat/chat.vue
+++ b/src/components/chat/chat.vue
@@ -22,13 +22,20 @@
:fallback-user="currentUser"
/>
-
From 0f3c667eb810a8fd3d41696507ef12e57669f8b1 Mon Sep 17 00:00:00 2001
From: eugenijm
Date: Mon, 11 May 2020 13:48:20 +0300
Subject: [PATCH 06/41] wip
---
src/components/chat/chat.scss | 9 ++++++++-
src/components/chat/chat.vue | 15 ++++++++-------
src/components/chat_title/chat_title.js | 2 +-
src/components/chat_title/chat_title.vue | 2 ++
4 files changed, 19 insertions(+), 9 deletions(-)
diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss
index 25e27cf5..98ca6a85 100644
--- a/src/components/chat/chat.scss
+++ b/src/components/chat/chat.scss
@@ -44,6 +44,12 @@
position: -webkit-sticky;
position: sticky;
+ .button-icon {
+ display: flex;
+ align-content: center;
+ align-items: center;
+ }
+
.go-back-button {
cursor: pointer;
margin-right: 0.7em;
@@ -66,8 +72,9 @@
text-overflow: ellipsis;
white-space: nowrap;
flex-shrink: 0;
- max-width: 70%;
+ max-width: 80%;
display: grid;
+ display: flex;
}
}
diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue
index 2aad5a49..108681a1 100644
--- a/src/components/chat/chat.vue
+++ b/src/components/chat/chat.vue
@@ -10,16 +10,17 @@
ref="header"
class="panel-heading direct-conversation-view-heading mobile-hidden"
>
-
-
-
+ > -->
+
+
@@ -29,13 +30,13 @@
width="23px"
height="23px"
/> -->
-
+ > -->
-
+
diff --git a/src/components/chat_title/chat_title.js b/src/components/chat_title/chat_title.js
index 4f9a47c4..dad5ede1 100644
--- a/src/components/chat_title/chat_title.js
+++ b/src/components/chat_title/chat_title.js
@@ -11,7 +11,7 @@ export default Vue.component('direct-conversation-title', {
ChatAvatar
},
props: [
- 'users', 'fallbackUser'
+ 'users', 'fallbackUser', 'withAvatar'
],
computed: {
...mapState({
diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue
index 0e6dbd30..e45a0b57 100644
--- a/src/components/chat_title/chat_title.vue
+++ b/src/components/chat_title/chat_title.vue
@@ -5,11 +5,13 @@
:title="title"
>
+
Date: Mon, 11 May 2020 13:55:06 +0300
Subject: [PATCH 07/41] WIP display the last message in the chat list
---
src/components/chat_list_item/chat_list_item.scss | 2 +-
src/components/chat_list_item/chat_list_item.vue | 5 ++++-
src/services/api/api.service.js | 2 +-
src/services/entity_normalizer/entity_normalizer.service.js | 3 ++-
4 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/src/components/chat_list_item/chat_list_item.scss b/src/components/chat_list_item/chat_list_item.scss
index ee9054bb..335f133d 100644
--- a/src/components/chat_list_item/chat_list_item.scss
+++ b/src/components/chat_list_item/chat_list_item.scss
@@ -68,7 +68,7 @@
text-overflow: ellipsis;
overflow: hidden;
flex: 1;
- margin-right: 10px;
+ margin-right: 15px;
}
.faint-link {
diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue
index 1acc49a5..07931759 100644
--- a/src/components/chat_list_item/chat_list_item.vue
+++ b/src/components/chat_list_item/chat_list_item.vue
@@ -27,7 +27,10 @@
- {{ 'Last message placeholder' }}
+
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
.then((data) => {
- return { chats: data.map(parseChat), pagination }
+ return { chats: data.map(parseChat).filter(c => c), pagination }
})
}
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 3d321d65..5f9be7f1 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -376,11 +376,12 @@ export const parseChat = (chat) => {
output.id = parseInt(chat.id, 10)
output.account = parseUser(chat.account)
output.unread = chat.unread
- output.lastMessage = undefined
+ output.lastMessage = parseChatMessage(chat.last_message)
return output
}
export const parseChatMessage = (message) => {
+ if (!message) { return }
let output = message
output.id = parseInt(message.id, 10)
output.created_at = new Date(message.created_at)
From 86cc4ce08d4ee8225933c5da9247b3586582a4ca Mon Sep 17 00:00:00 2001
From: eugenijm
Date: Mon, 11 May 2020 14:35:55 +0300
Subject: [PATCH 08/41] WIP attachments
---
src/components/chat/chat.js | 19 ++++++++++++++++---
src/components/chat/chat.vue | 1 -
src/components/chat_message/chat_message.js | 13 ++++++++++---
.../post_status_form/post_status_form.js | 1 -
.../post_status_form/post_status_form.vue | 9 +++++++--
src/i18n/en.json | 3 ++-
src/services/api/api.service.js | 14 ++++++++++----
.../entity_normalizer.service.js | 3 +++
8 files changed, 48 insertions(+), 15 deletions(-)
diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js
index 8f8e279d..27713e30 100644
--- a/src/components/chat/chat.js
+++ b/src/components/chat/chat.js
@@ -325,11 +325,24 @@ const Chat = {
})
this.fetchChat(true, chatId)
},
- poster ({ status }) {
- return this.backendInteractor.postChatMessage({
+ poster (opts) {
+ const status = opts.status
+
+ if (!status) {
+ // TODO:
+ return Promise.resolve({ error: this.$t('chats.empty_message_error') })
+ }
+
+ let params = {
id: this.currentChat.id,
content: status
- })
+ }
+
+ if (opts.media && opts.media[0]) {
+ params.mediaId = opts.media[0].id
+ }
+
+ return this.backendInteractor.postChatMessage(params)
}
}
}
diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue
index 108681a1..dd8d849e 100644
--- a/src/components/chat/chat.vue
+++ b/src/components/chat/chat.vue
@@ -76,7 +76,6 @@
:disable-subject="true"
:disable-scope-selector="true"
:disable-notice="true"
- :disable-attachments="true"
:disable-polls="true"
:poster="poster"
:preserve-focus="true"
diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js
index 7e3cfad3..5354aa00 100644
--- a/src/components/chat_message/chat_message.js
+++ b/src/components/chat_message/chat_message.js
@@ -46,12 +46,19 @@ const ChatMessage = {
return this.chatViewItem.type === 'message'
},
messageForStatusContent () {
- return {
+ let result = {
summary: '',
statusnet_html: this.message.content,
- text: this.message.content,
- attachments: []
+ text: this.message.content
}
+
+ if (this.message.attachment) {
+ result.attachments = [this.message.attachment]
+ } else {
+ result.attachments = []
+ }
+
+ return result
},
...mapState({
betterShadow: state => state.interface.browserSupport.cssFilter,
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 27fd9db1..dcb6220d 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -36,7 +36,6 @@ const PostStatusForm = {
'disableScopeSelector',
'disableNotice',
'disablePolls',
- 'disableAttachments',
'placeholder',
'maxHeight',
'poster',
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index bba79989..cc82f8f4 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -169,7 +169,6 @@
>
-
-
+
diff --git a/src/modules/chats.js b/src/modules/chats.js
index 3c29ebf7..0e18723a 100644
--- a/src/modules/chats.js
+++ b/src/modules/chats.js
@@ -1,5 +1,5 @@
import { set } from 'vue'
-import { find, omitBy, debounce, last } from 'lodash'
+import { find, omitBy, debounce, last, orderBy } from 'lodash'
import chatService from '../services/chat_service/chat_service.js'
import { parseChat, parseChatMessage } from '../services/entity_normalizer/entity_normalizer.service.js'
@@ -18,12 +18,21 @@ const defaultState = {
currentChatId: null
}
+const getChatById = (state, id) => {
+ return find(state.chatList.data, { id })
+}
+
+const sortedChatList = (state) => {
+ return orderBy(state.chatList.data, ['updated_at'], ['desc'])
+}
+
const chats = {
state: { ...defaultState },
getters: {
currentChat: state => state.openedChats[state.currentChatId],
currentChatMessageService: state => state.openedChatMessageServices[state.currentChatId],
- findOpenedChatByRecipientId: state => recipientId => find(state.openedChats, c => c.account.id === recipientId)
+ findOpenedChatByRecipientId: state => recipientId => find(state.openedChats, c => c.account.id === recipientId),
+ sortedChatList
},
actions: {
// Chat list
@@ -116,28 +125,27 @@ const chats = {
if (chats.length > 0) {
state.chatList.pagination = { maxId: last(chats).id }
}
- chats.forEach((conversation) => {
- // This is to prevent duplicate conversations being added
- // (right now, backend can return the same conversation on different pages)
- if (!state.chatList.idStore[conversation.id]) {
- state.chatList.data.push(conversation)
- state.chatList.idStore[conversation.id] = conversation
+ chats.forEach((updatedChat) => {
+ let chat = getChatById(state, updatedChat.id)
+
+ if (chat) {
+ chat.lastMessage = updatedChat.lastMessage
+ chat.unread = updatedChat.unread
} else {
- const chat = find(state.chatList.data, { id: conversation.id })
- chat.last_status = conversation.last_status
- chat.unread = conversation.unread
- state.chatList.idStore[conversation.id] = conversation
+ state.chatList.data.push(updatedChat)
+ set(state.chatList.idStore, updatedChat.id, updatedChat)
}
})
},
updateChat (state, { _dispatch, chat: updatedChat, _rootGetters }) {
- let chat = find(state.chatList.data, { id: updatedChat.id })
+ let chat = getChatById(state, updatedChat.id)
if (chat) {
chat.lastMessage = updatedChat.lastMessage
chat.unread = updatedChat.unread
+ chat.updated_at = updatedChat.updated_at
}
if (!chat) { state.chatList.data.unshift(updatedChat) }
- state.chatList.idStore[updatedChat.id] = updatedChat
+ set(state.chatList.idStore, updatedChat.id, updatedChat)
},
deleteChat (state, { _dispatch, id, _rootGetters }) {
state.chats.data = state.chats.data.filter(conversation =>
@@ -156,15 +164,16 @@ const chats = {
const chatMessageService = state.openedChatMessageServices[chatId]
if (chatMessageService) {
chatService.add(chatMessageService, { messages: messages.map(parseChatMessage) })
- commit('refreshLastMessage', { commit, chatId })
+ commit('refreshLastMessage', { chatId })
}
},
refreshLastMessage (state, { chatId }) {
const chatMessageService = state.openedChatMessageServices[chatId]
if (chatMessageService) {
- const chat = state.chatList.data.find(c => c.id === chatId)
+ let chat = getChatById(state, chatId)
if (chat) {
chat.lastMessage = chatMessageService.lastMessage
+ chat.updated_at = chatMessageService.lastMessage.created_at
}
}
},
@@ -172,7 +181,7 @@ const chats = {
const chatMessageService = state.openedChatMessageServices[chatId]
if (chatMessageService) {
chatService.deleteMessage(chatMessageService, messageId)
- commit('refreshLastMessage', { commit, chatId })
+ commit('refreshLastMessage', { chatId })
}
},
resetChatNewMessageCount (state, _value) {
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 5c2bf2a2..74645afb 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1124,22 +1124,19 @@ export const handleMastoWS = (wsEvent) => {
}
}
-const chats = ({ maxId, sinceId, limit = 20, recipients = [], credentials }) => {
+const chats = ({ maxId, sinceId, limit = 20, credentials }) => {
let url = PLEROMA_CHATS_URL
- const recipientIds = recipients.map(r => `recipients[]=${r}`).join('&')
const args = [
maxId && `max_id=${maxId}`,
sinceId && `since_id=${sinceId}`,
- limit && `limit=${limit}`,
- recipientIds && `${recipientIds}`
+ limit && `limit=${limit}`
].filter(_ => _).join('&')
- let pagination = {}
url = url + (args ? '?' + args : '')
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
.then((data) => {
- return { chats: data.map(parseChat).filter(c => c), pagination }
+ return { chats: data.map(parseChat).filter(c => c) }
})
}
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index ea97f245..2db403de 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -373,11 +373,12 @@ const isNsfw = (status) => {
}
export const parseChat = (chat) => {
- let output = chat
+ let output = {}
output.id = parseInt(chat.id, 10)
output.account = parseUser(chat.account)
output.unread = chat.unread
output.lastMessage = parseChatMessage(chat.last_message)
+ output.updated_at = new Date(chat.updated_at)
return output
}
From 6b272b9c99107408b2952f214b75800f3a372174 Mon Sep 17 00:00:00 2001
From: eugenijm
Date: Thu, 21 May 2020 08:17:40 +0300
Subject: [PATCH 22/41] WIP chat improvements
---
src/components/chat/chat.js | 7 ++++++-
src/components/chat/chat.scss | 14 +++++++++++++
src/components/chat/chat.vue | 37 +++++++++++------------------------
3 files changed, 31 insertions(+), 27 deletions(-)
diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js
index 16ec4fd9..43e5ff8e 100644
--- a/src/components/chat/chat.js
+++ b/src/components/chat/chat.js
@@ -60,7 +60,12 @@ const Chat = {
if (this.currentChat) {
return [this.currentChat.account]
} else {
- return []
+ const user = this.findUser(this.recipientId)
+ if (user) {
+ return [user]
+ } else {
+ return []
+ }
}
},
recipient () {
diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss
index 115918e4..a8b607ba 100644
--- a/src/components/chat/chat.scss
+++ b/src/components/chat/chat.scss
@@ -44,6 +44,20 @@
position: -webkit-sticky;
position: sticky;
+ .direct-conversation-title {
+ display: flex;
+
+ a {
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ .go-back-button-wrapper {
+ display: flex;
+ width: 100%;
+ }
+
.button-icon {
cursor: pointer;
display: flex;
diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue
index d070a02f..9db3f4b6 100644
--- a/src/components/chat/chat.vue
+++ b/src/components/chat/chat.vue
@@ -10,36 +10,21 @@
ref="header"
class="panel-heading direct-conversation-view-heading mobile-hidden"
>
-
-
-
-
-
-
-
From c1faeacee86ee68bab2aaabe3dc814b636426242 Mon Sep 17 00:00:00 2001
From: eugenijm
Date: Thu, 21 May 2020 15:48:35 +0300
Subject: [PATCH 23/41] Set file limit in the chat posting form
---
src/components/chat/chat.vue | 1 +
src/components/media_upload/media_upload.js | 3 ++-
src/components/media_upload/media_upload.vue | 17 +++++++++++++++++
.../post_status_form/post_status_form.js | 3 ++-
.../post_status_form/post_status_form.vue | 1 +
5 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue
index 9db3f4b6..cd4c047a 100644
--- a/src/components/chat/chat.vue
+++ b/src/components/chat/chat.vue
@@ -70,6 +70,7 @@
:polls-available="false"
:auto-focus="true"
:placeholder="formPlaceholder"
+ :file-limit="1"
max-height="160"
@resize="handleResize"
@posted="onPosted"
diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js
index f457d022..9e4bc536 100644
--- a/src/components/media_upload/media_upload.js
+++ b/src/components/media_upload/media_upload.js
@@ -62,7 +62,8 @@ const mediaUpload = {
}
},
props: [
- 'dropFiles'
+ 'dropFiles',
+ 'disabled'
],
watch: {
'dropFiles': function (fileInfos) {
diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue
index 0fc305ac..d1d842dc 100644
--- a/src/components/media_upload/media_upload.vue
+++ b/src/components/media_upload/media_upload.vue
@@ -1,6 +1,7 @@
diff --git a/src/components/chat/chat_layout.js b/src/components/chat/chat_layout.js
new file mode 100644
index 00000000..07ae3abf
--- /dev/null
+++ b/src/components/chat/chat_layout.js
@@ -0,0 +1,100 @@
+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
new file mode 100644
index 00000000..f07ba2a1
--- /dev/null
+++ b/src/components/chat/chat_layout_utils.js
@@ -0,0 +1,27 @@
+// Captures a scroll position
+export const getScrollPosition = (el) => {
+ return {
+ scrollTop: el.scrollTop,
+ scrollHeight: el.scrollHeight,
+ offsetHeight: el.offsetHeight
+ }
+}
+
+// A helper function that is used to keep the scroll position fixed as the new elements are added to the top
+// Takes two scroll positions, before and after the update.
+export const getNewTopPosition = (previousPosition, newPosition) => {
+ return previousPosition.scrollTop + (newPosition.scrollHeight - previousPosition.scrollHeight)
+}
+
+export const isBottomedOut = (el, offset = 0) => {
+ if (!el) { return }
+ const scrollHeight = el.scrollTop + offset
+ const totalHeight = el.scrollHeight - el.offsetHeight
+ return totalHeight <= scrollHeight
+}
+
+// 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
+}
diff --git a/src/components/chat_avatar/chat_avatar.js b/src/components/chat_avatar/chat_avatar.js
new file mode 100644
index 00000000..7b26e07c
--- /dev/null
+++ b/src/components/chat_avatar/chat_avatar.js
@@ -0,0 +1,23 @@
+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
new file mode 100644
index 00000000..f54a7151
--- /dev/null
+++ b/src/components/chat_avatar/chat_avatar.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/chat_list/chat_list.js b/src/components/chat_list/chat_list.js
new file mode 100644
index 00000000..95708d1d
--- /dev/null
+++ b/src/components/chat_list/chat_list.js
@@ -0,0 +1,37 @@
+import { mapState, mapGetters } from 'vuex'
+import ChatListItem from '../chat_list_item/chat_list_item.vue'
+import ChatNew from '../chat_new/chat_new.vue'
+import List from '../list/list.vue'
+
+const ChatList = {
+ components: {
+ ChatListItem,
+ List,
+ ChatNew
+ },
+ computed: {
+ ...mapState({
+ currentUser: state => state.users.currentUser
+ }),
+ ...mapGetters(['sortedChatList'])
+ },
+ data () {
+ return {
+ isNew: false
+ }
+ },
+ created () {
+ this.$store.dispatch('fetchChats', { latest: true })
+ },
+ methods: {
+ cancelNewChat () {
+ this.isNew = false
+ this.$store.dispatch('fetchChats', { latest: true })
+ },
+ newChat () {
+ this.isNew = true
+ }
+ }
+}
+
+export default ChatList
diff --git a/src/components/chat_list/chat_list.vue b/src/components/chat_list/chat_list.vue
new file mode 100644
index 00000000..e62f58e5
--- /dev/null
+++ b/src/components/chat_list/chat_list.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+ {{ $t("chats.chats") }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/chat_list_item/chat_list_item.js b/src/components/chat_list_item/chat_list_item.js
new file mode 100644
index 00000000..1c27088c
--- /dev/null
+++ b/src/components/chat_list_item/chat_list_item.js
@@ -0,0 +1,65 @@
+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 AvatarList from '../avatar_list/avatar_list.vue'
+import Timeago from '../timeago/timeago.vue'
+import ChatTitle from '../chat_title/chat_title.vue'
+
+const ChatListItem = {
+ name: 'ChatListItem',
+ props: [
+ 'chat'
+ ],
+ components: {
+ ChatAvatar,
+ AvatarList,
+ Timeago,
+ ChatTitle,
+ StatusContent
+ },
+ computed: {
+ ...mapState({
+ currentUser: state => state.users.currentUser
+ }),
+ attachmentInfo () {
+ if (this.chat.lastMessage.attachments.length === 0) { return }
+
+ const types = this.chat.lastMessage.attachments.map(file => fileType.fileType(file.mimetype))
+ if (types.includes('video')) {
+ return this.$t('file_type.video')
+ } else if (types.includes('audio')) {
+ return this.$t('file_type.audio')
+ } else if (types.includes('image')) {
+ return this.$t('file_type.image')
+ } else {
+ return this.$t('file_type.file')
+ }
+ },
+ messageForStatusContent () {
+ const content = this.chat.lastMessage ? (this.attachmentInfo || this.chat.lastMessage.content) : ''
+
+ return {
+ summary: '',
+ statusnet_html: content,
+ text: content,
+ attachments: []
+ }
+ }
+ },
+ methods: {
+ openChat (_e) {
+ if (this.chat.id) {
+ this.$router.push({
+ name: 'chat',
+ params: {
+ username: this.currentUser.screen_name,
+ recipient_id: this.chat.account.id
+ }
+ })
+ }
+ }
+ }
+}
+
+export default ChatListItem
diff --git a/src/components/chat_list_item/chat_list_item.scss b/src/components/chat_list_item/chat_list_item.scss
new file mode 100644
index 00000000..12269f89
--- /dev/null
+++ b/src/components/chat_list_item/chat_list_item.scss
@@ -0,0 +1,94 @@
+.chat-list-item {
+ &:hover .animated.avatar {
+ canvas {
+ display: none;
+ }
+ img {
+ visibility: visible;
+ }
+ }
+
+ display: flex;
+ flex-direction: row;
+ padding: 0.75em;
+ height: 4.85em;
+ overflow: hidden;
+ box-sizing: border-box;
+ cursor: pointer;
+
+ :focus {
+ outline: none;
+ }
+
+ &:hover {
+ background-color: var(--selectedPost, $fallback--lightBg);
+ box-shadow: 0 0px 3px 1px rgba(0, 0, 0, 0.1);
+ }
+
+ .chat-list-item-left {
+ margin-right: 1em;
+ }
+
+ .chat-list-item-center {
+ width: 100%;
+ box-sizing: border-box;
+ overflow: hidden;
+ word-wrap: break-word;
+ }
+
+ .heading {
+ width: 100%;
+ display: inline-flex;
+ justify-content: space-between;
+ line-height: 1em;
+ }
+
+ .heading-right {
+ 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;
+ overflow: hidden;
+ flex-shrink: 1;
+ }
+
+ .chat-preview {
+ display: inline-flex;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ margin: 0.35rem 0;
+ height: 1.2em;
+ line-height: 1.2em;
+ color: $fallback--text;
+ color: var(--faint, $fallback--text);
+ }
+
+ a {
+ color: var(--faintLink, $fallback--link);
+ text-decoration: none;
+ pointer-events: none;
+ }
+
+ .unread-indicator-wrapper {
+ display: flex;
+ align-items: center;
+ margin-left: 10px;
+ }
+
+ .unread-indicator {
+ border-radius: 100%;
+ height: 8px;
+ width: 8px;
+ background-color: $fallback--link;
+ background-color: var(--link, $fallback--link);
+ }
+}
diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue
new file mode 100644
index 00000000..26ad581b
--- /dev/null
+++ b/src/components/chat_list_item/chat_list_item.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ chat.unread }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js
new file mode 100644
index 00000000..aba95074
--- /dev/null
+++ b/src/components/chat_message/chat_message.js
@@ -0,0 +1,109 @@
+import { mapState, mapGetters } from 'vuex'
+import Popover from '../popover/popover.vue'
+import Attachment from '../attachment/attachment.vue'
+import UserAvatar from '../user_avatar/user_avatar.vue'
+import Gallery from '../gallery/gallery.vue'
+import LinkPreview from '../link-preview/link-preview.vue'
+import StatusContent from '../status_content/status_content.vue'
+import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
+import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
+
+const ChatMessage = {
+ name: 'ChatMessage',
+ props: [
+ 'author',
+ 'edited',
+ 'noHeading',
+ 'chatViewItem',
+ 'hoveredMessageChain'
+ ],
+ components: {
+ Popover,
+ Attachment,
+ StatusContent,
+ UserAvatar,
+ Gallery,
+ LinkPreview,
+ ChatMessageDate
+ },
+ computed: {
+ // Returns HH:MM (hours and minutes) in local time.
+ createdAt () {
+ const time = this.chatViewItem.data.created_at
+ return time.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hour12: false })
+ },
+ isCurrentUser () {
+ return this.message.account_id === this.currentUser.id
+ },
+ message () {
+ return this.chatViewItem.data
+ },
+ userProfileLink () {
+ return generateProfileLink(this.author.id, this.author.screen_name, this.$store.state.instance.restrictedNicknames)
+ },
+ isMessage () {
+ return this.chatViewItem.type === 'message'
+ },
+ messageForStatusContent () {
+ return {
+ summary: '',
+ statusnet_html: this.message.content,
+ text: this.message.content,
+ attachments: this.message.attachments
+ }
+ },
+ hasAttachment () {
+ return this.message.attachments.length > 0
+ },
+ ...mapState({
+ betterShadow: state => state.interface.browserSupport.cssFilter,
+ currentUser: state => state.users.currentUser,
+ restrictedNicknames: state => state.instance.restrictedNicknames
+ }),
+ ellipsisButtonWrapperStyle () {
+ let res = {
+ 'opacity': this.hovered || this.menuOpened ? '1' : '0'
+ }
+
+ if (this.isCurrentUser) {
+ res.right = '5px'
+ } else {
+ res.left = '5px'
+ }
+
+ return res
+ },
+ popoverMarginStyle () {
+ if (this.isCurrentUser) {
+ return {}
+ } else {
+ return { left: 50 }
+ }
+ },
+ ...mapGetters(['mergedConfig', 'findUser'])
+ },
+ data () {
+ return {
+ hovered: false,
+ menuOpened: false
+ }
+ },
+ methods: {
+ onHover (bool) {
+ this.$emit('hover', { isHovered: bool, messageChainId: this.chatViewItem.messageChainId })
+ },
+ async deleteMessage () {
+ const confirmed = window.confirm(this.$t('chats.delete_confirm'))
+ if (confirmed) {
+ await this.$store.dispatch('deleteChatMessage', {
+ messageId: this.chatViewItem.data.id,
+ chatId: this.chatViewItem.data.chat_id
+ })
+ }
+ this.hovered = false
+ this.menuOpened = false
+ }
+ }
+}
+
+export default ChatMessage
diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss
new file mode 100644
index 00000000..e4028537
--- /dev/null
+++ b/src/components/chat_message/chat_message.scss
@@ -0,0 +1,157 @@
+@import '../../_variables.scss';
+
+.chat-message-wrapper {
+ &.hovered-message-chain {
+ .animated.avatar {
+ canvas {
+ display: none;
+ }
+ img {
+ visibility: visible;
+ }
+ }
+ }
+
+ &:last-child {
+ margin-bottom: 16px;
+ }
+
+ .chat-message-menu {
+ transition: opacity 0.1s;
+ opacity: 0;
+ position: absolute;
+ top: -10px;
+
+ button {
+ padding-top: 3px;
+ padding-bottom: 3px;
+ }
+ }
+
+ .icon-ellipsis {
+ cursor: pointer;
+
+ &:hover, .extra-button-popover.open & {
+ color: $fallback--text;
+ color: var(--text, $fallback--text);
+ }
+
+ border-radius: $fallback--chatMessageRadius;
+ border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
+ }
+
+ .popover {
+ width: 12rem;
+ }
+
+ .chat-message {
+ display: flex;
+ padding-bottom: 7px;
+ }
+
+ .avatar-wrapper {
+ margin-right: 10px;
+ width: 32px;
+ }
+
+ .link-preview, .attachments {
+ margin-bottom: 0.9em;
+ }
+
+ .chat-message-inner {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ max-width: 80%;
+ min-width: 10rem;
+ width: 100%;
+
+ &.with-media {
+ width: 100%;
+
+ .gallery-row {
+ overflow: hidden;
+ }
+
+ .status {
+ width: 100%;
+ }
+ }
+ }
+
+ .status {
+ border-radius: $fallback--chatMessageRadius;
+ border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
+ display: flex;
+ padding: 0.75em;
+ }
+
+ .created-at {
+ float: right;
+ font-size: 0.8em;
+ margin: -10px 0 -5px 4px;
+ font-style: italic;
+ opacity: 0.8;
+ }
+
+ .without-attachment {
+ .status-content {
+ white-space: normal;
+
+ &::after {
+ margin-right: 75px;
+ content: " ";
+ display: inline-block;
+ }
+ }
+ }
+
+ .incoming {
+ a {
+ color: var(--chatMessageIncomingLink, $fallback--link);
+ }
+
+ .status {
+ color: var(--chatMessageIncomingText, $fallback--text);
+ background-color: var(--chatMessageIncomingBg, $fallback--bg);
+ border: 1px solid var(--chatMessageIncomingBorder, --border);
+ }
+
+ .created-at {
+ a {
+ color: var(--chatMessageIncomingText, $fallback--text);
+ }
+ }
+ }
+
+ .outgoing {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-content: end;
+ justify-content: flex-end;
+
+ a {
+ color: var(--chatMessageOutgoingLink, $fallback--link);
+ }
+
+ .status {
+ color: var(--chatMessageOutgoingText, $fallback--text);
+ background-color: var(--chatMessageOutgoingBg, $fallback--lightBg);
+ border: 1px solid var(--chatMessageOutgoingBorder, --lightBg);
+ }
+
+ .chat-message-inner {
+ align-items: flex-end;
+ }
+ }
+}
+
+.chat-message-date-separator {
+ text-align: center;
+ margin: 1.4em 0;
+ font-size: 0.9em;
+ user-select: none;
+ color: $fallback--text;
+ color: var(--faintedText, $fallback--text);
+}
diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue
new file mode 100644
index 00000000..872ddf70
--- /dev/null
+++ b/src/components/chat_message/chat_message.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ createdAt }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/chat_message_date/chat_message_date.vue b/src/components/chat_message_date/chat_message_date.vue
new file mode 100644
index 00000000..79c346b6
--- /dev/null
+++ b/src/components/chat_message_date/chat_message_date.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
diff --git a/src/components/chat_new/chat_new.js b/src/components/chat_new/chat_new.js
new file mode 100644
index 00000000..0da681f7
--- /dev/null
+++ b/src/components/chat_new/chat_new.js
@@ -0,0 +1,74 @@
+import { throttle } from 'lodash'
+import { mapState, mapGetters } from 'vuex'
+import BasicUserCard from '../basic_user_card/basic_user_card.vue'
+import UserAvatar from '../user_avatar/user_avatar.vue'
+
+const chatNew = {
+ components: {
+ BasicUserCard,
+ UserAvatar
+ },
+ data () {
+ return {
+ suggestions: [],
+ userIds: [],
+ loading: false,
+ query: ''
+ }
+ },
+ async created () {
+ const { chats } = await this.backendInteractor.chats()
+ chats.forEach(chat => this.suggestions.push(chat.account))
+ },
+ computed: {
+ users () {
+ return this.userIds.map(userId => this.findUser(userId))
+ },
+ availableUsers () {
+ if (this.query.length !== 0) {
+ return this.users
+ } else {
+ return this.suggestions
+ }
+ },
+ ...mapState({
+ currentUser: state => state.users.currentUser,
+ backendInteractor: state => state.api.backendInteractor
+ }),
+ ...mapGetters(['findUser'])
+ },
+ methods: {
+ goBack () {
+ this.$emit('cancel')
+ },
+ goToChat (user) {
+ this.$router.push({ name: 'chat', params: { recipient_id: user.id } })
+ },
+ onInput () {
+ this.search(this.query)
+ },
+ addUser (user) {
+ this.selectedUserIds.push(user.id)
+ this.query = ''
+ },
+ removeUser (userId) {
+ this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId)
+ },
+ search: throttle(function (query) {
+ if (!query) {
+ this.loading = false
+ return
+ }
+
+ this.loading = true
+ this.userIds = []
+ this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts' })
+ .then(data => {
+ this.loading = false
+ this.userIds = data.accounts.map(a => a.id)
+ })
+ })
+ }
+}
+
+export default chatNew
diff --git a/src/components/chat_new/chat_new.scss b/src/components/chat_new/chat_new.scss
new file mode 100644
index 00000000..39216677
--- /dev/null
+++ b/src/components/chat_new/chat_new.scss
@@ -0,0 +1,29 @@
+.chat-new {
+ .input-wrap {
+ display: flex;
+ margin: 0.7em 0.5em 0.7em 0.5em;
+
+ input {
+ width: 100%;
+ }
+ }
+
+ .icon-search {
+ font-size: 1.5em;
+ float: right;
+ margin-right: 0.3em;
+ }
+
+ .member-list {
+ padding-bottom: 0.67rem;
+ }
+
+ .basic-user-card:hover {
+ cursor: pointer;
+ background-color: var(--selectedPost, $fallback--lightBg);
+ }
+
+ .go-back-button {
+ cursor: pointer;
+ }
+}
diff --git a/src/components/chat_new/chat_new.vue b/src/components/chat_new/chat_new.vue
new file mode 100644
index 00000000..3333dbf9
--- /dev/null
+++ b/src/components/chat_new/chat_new.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+
diff --git a/src/components/chat_panel/chat_panel.vue b/src/components/chat_panel/chat_panel.vue
index 3677722f..12968cfb 100644
--- a/src/components/chat_panel/chat_panel.vue
+++ b/src/components/chat_panel/chat_panel.vue
@@ -84,54 +84,56 @@
max-width: 25em;
}
-.chat-heading {
- cursor: pointer;
- .icon-comment-empty {
- color: $fallback--text;
- color: var(--text, $fallback--text);
- }
-}
-
-.chat-window {
- overflow-y: auto;
- overflow-x: hidden;
- max-height: 20em;
-}
-
-.chat-window-container {
- height: 100%;
-}
-
-.chat-message {
- display: flex;
- padding: 0.2em 0.5em
-}
-
-.chat-avatar {
- img {
- height: 24px;
- width: 24px;
- border-radius: $fallback--avatarRadius;
- border-radius: var(--avatarRadius, $fallback--avatarRadius);
- margin-right: 0.5em;
- margin-top: 0.25em;
- }
-}
-
-.chat-input {
- display: flex;
- textarea {
- flex: 1;
- margin: 0.6em;
- min-height: 3.5em;
- resize: none;
- }
-}
-
.chat-panel {
- .title {
+ .chat-heading {
+ cursor: pointer;
+ .icon-comment-empty {
+ color: $fallback--text;
+ color: var(--text, $fallback--text);
+ }
+ }
+
+ .chat-window {
+ overflow-y: auto;
+ overflow-x: hidden;
+ max-height: 20em;
+ }
+
+ .chat-window-container {
+ height: 100%;
+ }
+
+ .chat-message {
display: flex;
- justify-content: space-between;
+ padding: 0.2em 0.5em
+ }
+
+ .chat-avatar {
+ img {
+ height: 24px;
+ width: 24px;
+ border-radius: $fallback--avatarRadius;
+ border-radius: var(--avatarRadius, $fallback--avatarRadius);
+ margin-right: 0.5em;
+ margin-top: 0.25em;
+ }
+ }
+
+ .chat-input {
+ display: flex;
+ textarea {
+ flex: 1;
+ margin: 0.6em;
+ min-height: 3.5em;
+ resize: none;
+ }
+ }
+
+ .chat-panel {
+ .title {
+ display: flex;
+ justify-content: space-between;
+ }
}
}
diff --git a/src/components/chat_title/chat_title.js b/src/components/chat_title/chat_title.js
new file mode 100644
index 00000000..2723d5f5
--- /dev/null
+++ b/src/components/chat_title/chat_title.js
@@ -0,0 +1,20 @@
+import Vue from 'vue'
+import ChatAvatar from '../chat_avatar/chat_avatar.vue'
+
+export default Vue.component('chat-title', {
+ name: 'ChatTitle',
+ components: {
+ ChatAvatar
+ },
+ props: [
+ 'user', 'withAvatar'
+ ],
+ computed: {
+ title () {
+ return this.user ? this.user.screen_name : ''
+ },
+ htmlTitle () {
+ return this.user ? this.user.name_html : ''
+ }
+ }
+})
diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue
new file mode 100644
index 00000000..fd42d125
--- /dev/null
+++ b/src/components/chat_title/chat_title.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index 7974a66d..a27da090 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -79,6 +79,15 @@ const EmojiInput = {
required: false,
type: Boolean,
default: false
+ },
+ placement: {
+ /**
+ * Forces the panel to take a specific position relative to the input element.
+ * The 'auto' placement chooses either bottom or top depending on which has the available space (when both have available space, bottom is preferred).
+ */
+ required: false,
+ type: String, // 'auto', 'top', 'bottom'
+ default: 'auto'
}
},
data () {
@@ -162,6 +171,11 @@ const EmojiInput = {
input.elm.removeEventListener('input', this.onInput)
}
},
+ watch: {
+ showSuggestions: function (newValue) {
+ this.$emit('shown', newValue)
+ }
+ },
methods: {
triggerShowPicker () {
this.showPicker = true
@@ -425,15 +439,29 @@ const EmojiInput = {
this.caret = selectionStart
},
resize () {
- const { panel, picker } = this.$refs
+ const panel = this.$refs.panel
if (!panel) return
+ const picker = this.$refs.picker.$el
+ const panelBody = this.$refs['panel-body']
const { offsetHeight, offsetTop } = this.input.elm
const offsetBottom = offsetTop + offsetHeight
- panel.style.top = offsetBottom + 'px'
- if (!picker) return
- picker.$el.style.top = offsetBottom + 'px'
- picker.$el.style.bottom = 'auto'
+ this.setPlacement(panelBody, panel, offsetBottom)
+ this.setPlacement(picker, picker, offsetBottom)
+ },
+ setPlacement (container, target, offsetBottom) {
+ if (!container || !target) return
+
+ target.style.top = offsetBottom + 'px'
+ target.style.bottom = 'auto'
+
+ if (this.placement === 'top' || (this.placement === 'auto' && this.overflowsBottom(container))) {
+ target.style.top = 'auto'
+ target.style.bottom = this.input.elm.offsetHeight + 'px'
+ }
+ },
+ overflowsBottom (el) {
+ return el.getBoundingClientRect().bottom > window.innerHeight
}
}
}
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index e9ac09c3..b9a74572 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -29,7 +29,10 @@
class="autocomplete-panel"
:class="{ hide: !showSuggestions }"
>
-
+
{{ $t('features_panel.chat') }}
+
+ {{ $t('features_panel.pleroma_chat_messages') }}
+
{{ $t('features_panel.gopher') }}
diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js
index fbb2d03d..7b8a76cc 100644
--- a/src/components/media_upload/media_upload.js
+++ b/src/components/media_upload/media_upload.js
@@ -61,7 +61,8 @@ const mediaUpload = {
}
},
props: [
- 'dropFiles'
+ 'dropFiles',
+ 'disabled'
],
watch: {
'dropFiles': function (fileInfos) {
diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue
index 5e31730b..d719eae1 100644
--- a/src/components/media_upload/media_upload.vue
+++ b/src/components/media_upload/media_upload.vue
@@ -1,5 +1,8 @@
-
+
state.instance.pleromaChatMessagesAvailable
+ }),
+ ...mapGetters(['unreadChatCount'])
},
methods: {
toggleDrawer () {
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 0ac53b34..4fdb3d13 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -40,12 +40,24 @@
{{ $t("nav.dms") }}
+
+ {{ $t("nav.chats") }}
+
+ {{ unreadChatCount }}
+
+
{{ $t("nav.twkn") }}
-
-
- {{ $t("nav.chat") }}
-
-
{
try {
- const { state, dispatch, rootState } = store
+ const { state, commit, dispatch, rootState } = store
const timelineData = rootState.statuses.timelines.friends
state.mastoUserSocket = state.backendInteractor.startUserSocket({ store })
state.mastoUserSocket.addEventListener(
@@ -66,11 +71,22 @@ const api = {
showImmediately: timelineData.visibleStatuses.length === 0,
timeline: 'friends'
})
+ } else if (message.event === 'pleroma:chat_update') {
+ dispatch('addChatMessages', {
+ chatId: message.chatUpdate.id,
+ messages: [message.chatUpdate.lastMessage]
+ })
+ dispatch('updateChat', { chat: message.chatUpdate })
}
}
)
+ state.mastoUserSocket.addEventListener('open', () => {
+ commit('setMastoUserSocketStatus', WSConnectionStatus.JOINED)
+ })
state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
console.error('Error in MastoAPI websocket:', error)
+ commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR)
+ dispatch('clearOpenedChats')
})
state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
const ignoreCodes = new Set([
@@ -84,8 +100,11 @@ const api = {
console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`)
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
+ dispatch('startFetchingChats')
dispatch('restartMastoUserSocket')
}
+ commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED)
+ dispatch('clearOpenedChats')
})
resolve()
} catch (e) {
@@ -99,12 +118,13 @@ const api = {
return dispatch('startMastoUserSocket').then(() => {
dispatch('stopFetchingTimeline', { timeline: 'friends' })
dispatch('stopFetchingNotifications')
+ dispatch('stopFetchingChats')
})
},
stopMastoUserSocket ({ state, dispatch }) {
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
- console.log(state.mastoUserSocket)
+ dispatch('startFetchingChats')
state.mastoUserSocket.close()
},
diff --git a/src/modules/chats.js b/src/modules/chats.js
new file mode 100644
index 00000000..f868ca0c
--- /dev/null
+++ b/src/modules/chats.js
@@ -0,0 +1,228 @@
+import Vue from 'vue'
+import { find, omitBy, orderBy, sumBy } from 'lodash'
+import chatService from '../services/chat_service/chat_service.js'
+import { parseChat, parseChatMessage } from '../services/entity_normalizer/entity_normalizer.service.js'
+
+const emptyChatList = () => ({
+ data: [],
+ idStore: {}
+})
+
+const defaultState = {
+ chatList: emptyChatList(),
+ chatListFetcher: null,
+ openedChats: {},
+ openedChatMessageServices: {},
+ fetcher: undefined,
+ currentChatId: null
+}
+
+const getChatById = (state, id) => {
+ return find(state.chatList.data, { id })
+}
+
+const sortedChatList = (state) => {
+ return orderBy(state.chatList.data, ['updated_at'], ['desc'])
+}
+
+const unreadChatCount = (state) => {
+ return sumBy(state.chatList.data, 'unread')
+}
+
+const chats = {
+ state: { ...defaultState },
+ getters: {
+ currentChat: state => state.openedChats[state.currentChatId],
+ currentChatMessageService: state => state.openedChatMessageServices[state.currentChatId],
+ findOpenedChatByRecipientId: state => recipientId => find(state.openedChats, c => c.account.id === recipientId),
+ sortedChatList,
+ unreadChatCount
+ },
+ actions: {
+ // Chat list
+ startFetchingChats ({ dispatch, commit }) {
+ const fetcher = () => {
+ dispatch('fetchChats', { latest: true })
+ }
+ fetcher()
+ commit('setChatListFetcher', {
+ fetcher: () => setInterval(() => { fetcher() }, 5000)
+ })
+ },
+ stopFetchingChats ({ commit }) {
+ commit('setChatListFetcher', { fetcher: undefined })
+ },
+ fetchChats ({ dispatch, rootState, commit }, params = {}) {
+ return rootState.api.backendInteractor.chats()
+ .then(({ chats }) => {
+ dispatch('addNewChats', { chats })
+ return chats
+ })
+ },
+ addNewChats ({ rootState, commit, dispatch, rootGetters }, { chats }) {
+ commit('addNewChats', { dispatch, chats, rootGetters })
+ },
+ updateChat ({ commit }, { chat }) {
+ commit('updateChat', { chat })
+ },
+
+ // Opened Chats
+ startFetchingCurrentChat ({ commit, dispatch }, { fetcher }) {
+ dispatch('setCurrentChatFetcher', { fetcher })
+ },
+ setCurrentChatFetcher ({ rootState, commit }, { fetcher }) {
+ commit('setCurrentChatFetcher', { fetcher })
+ },
+ addOpenedChat ({ rootState, commit, dispatch }, { chat }) {
+ commit('addOpenedChat', { dispatch, chat: parseChat(chat) })
+ dispatch('addNewUsers', [chat.account])
+ },
+ addChatMessages ({ commit }, value) {
+ commit('addChatMessages', { commit, ...value })
+ },
+ resetChatNewMessageCount ({ commit }, value) {
+ commit('resetChatNewMessageCount', value)
+ },
+ removeFromCurrentChatStatuses ({ commit }, { id }) {
+ commit('removeFromCurrentChatStatuses', id)
+ },
+ clearCurrentChat ({ rootState, commit, dispatch }, value) {
+ commit('setCurrentChatId', { chatId: undefined })
+ commit('setCurrentChatFetcher', { fetcher: undefined })
+ },
+ readChat ({ rootState, commit, dispatch }, { id, lastReadId }) {
+ dispatch('resetChatNewMessageCount')
+ commit('readChat', { id })
+ rootState.api.backendInteractor.readChat({ id, lastReadId })
+ },
+ deleteChatMessage ({ rootState, commit }, value) {
+ rootState.api.backendInteractor.deleteChatMessage(value)
+ commit('deleteChatMessage', { commit, ...value })
+ },
+ resetChats ({ commit, dispatch }) {
+ dispatch('clearCurrentChat')
+ commit('resetChats', { commit })
+ },
+ clearOpenedChats ({ rootState, commit, dispatch, rootGetters }) {
+ commit('clearOpenedChats', { commit })
+ }
+ },
+ mutations: {
+ setChatListFetcher (state, { commit, fetcher }) {
+ const prevFetcher = state.chatListFetcher
+ if (prevFetcher) {
+ clearInterval(prevFetcher)
+ }
+ state.chatListFetcher = fetcher && fetcher()
+ },
+ setCurrentChatFetcher (state, { fetcher }) {
+ const prevFetcher = state.fetcher
+ if (prevFetcher) {
+ clearInterval(prevFetcher)
+ }
+ state.fetcher = fetcher && fetcher()
+ },
+ addOpenedChat (state, { _dispatch, chat }) {
+ state.currentChatId = chat.id
+ Vue.set(state.openedChats, chat.id, chat)
+
+ if (!state.openedChatMessageServices[chat.id]) {
+ Vue.set(state.openedChatMessageServices, chat.id, chatService.empty(chat.id))
+ }
+ },
+ setCurrentChatId (state, { chatId }) {
+ state.currentChatId = chatId
+ },
+ addNewChats (state, { _dispatch, chats, _rootGetters }) {
+ chats.forEach((updatedChat) => {
+ const chat = getChatById(state, updatedChat.id)
+
+ if (chat) {
+ chat.lastMessage = updatedChat.lastMessage
+ chat.unread = updatedChat.unread
+ } else {
+ state.chatList.data.push(updatedChat)
+ Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
+ }
+ })
+ },
+ updateChat (state, { _dispatch, chat: updatedChat, _rootGetters }) {
+ const chat = getChatById(state, updatedChat.id)
+ if (chat) {
+ chat.lastMessage = updatedChat.lastMessage
+ chat.unread = updatedChat.unread
+ chat.updated_at = updatedChat.updated_at
+ }
+ if (!chat) { state.chatList.data.unshift(updatedChat) }
+ Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
+ },
+ deleteChat (state, { _dispatch, id, _rootGetters }) {
+ state.chats.data = state.chats.data.filter(conversation =>
+ conversation.last_status.id !== id
+ )
+ state.chats.idStore = omitBy(state.chats.idStore, conversation => conversation.last_status.id === id)
+ },
+ resetChats (state, { commit }) {
+ state.chatList = emptyChatList()
+ state.currentChatId = null
+ commit('setChatListFetcher', { fetcher: undefined })
+ for (const chatId in state.openedChats) {
+ chatService.clear(state.openedChatMessageServices[chatId])
+ Vue.delete(state.openedChats, chatId)
+ Vue.delete(state.openedChatMessageServices, chatId)
+ }
+ },
+ setChatsLoading (state, { value }) {
+ state.chats.loading = value
+ },
+ addChatMessages (state, { commit, chatId, messages }) {
+ const chatMessageService = state.openedChatMessageServices[chatId]
+ if (chatMessageService) {
+ chatService.add(chatMessageService, { messages: messages.map(parseChatMessage) })
+ commit('refreshLastMessage', { chatId })
+ }
+ },
+ refreshLastMessage (state, { chatId }) {
+ const chatMessageService = state.openedChatMessageServices[chatId]
+ if (chatMessageService) {
+ const chat = getChatById(state, chatId)
+ if (chat) {
+ chat.lastMessage = chatMessageService.lastMessage
+ if (chatMessageService.lastMessage) {
+ chat.updated_at = chatMessageService.lastMessage.created_at
+ }
+ }
+ }
+ },
+ deleteChatMessage (state, { commit, chatId, messageId }) {
+ const chatMessageService = state.openedChatMessageServices[chatId]
+ if (chatMessageService) {
+ chatService.deleteMessage(chatMessageService, messageId)
+ commit('refreshLastMessage', { chatId })
+ }
+ },
+ resetChatNewMessageCount (state, _value) {
+ const chatMessageService = state.openedChatMessageServices[state.currentChatId]
+ chatService.resetNewMessageCount(chatMessageService)
+ },
+ // Used when a connection loss occurs
+ clearOpenedChats (state) {
+ const currentChatId = state.currentChatId
+ for (const chatId in state.openedChats) {
+ if (currentChatId !== chatId) {
+ chatService.clear(state.openedChatMessageServices[chatId])
+ Vue.delete(state.openedChats, chatId)
+ Vue.delete(state.openedChatMessageServices, chatId)
+ }
+ }
+ },
+ readChat (state, { id }) {
+ const chat = getChatById(state, id)
+ if (chat) {
+ chat.unread = 0
+ }
+ }
+ }
+}
+
+export default chats
diff --git a/src/modules/config.js b/src/modules/config.js
index 47b24d77..e0fe72df 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -46,7 +46,8 @@ export const defaultState = {
repeats: true,
moves: true,
emojiReactions: false,
- followRequest: true
+ followRequest: true,
+ chatMention: true
},
webPushNotifications: false,
muteWords: [],
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 45a8eeca..3fe3bbf3 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -55,6 +55,7 @@ const defaultState = {
// Feature-set, apparently, not everything here is reported...
chatAvailable: false,
+ pleromaChatMessagesAvailable: false,
gopherAvailable: false,
mediaProxyAvailable: false,
suggestionsEnabled: false,
diff --git a/src/modules/interface.js b/src/modules/interface.js
index e31630fc..ec08ac0a 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -15,7 +15,8 @@ const defaultState = {
)
},
mobileLayout: false,
- globalNotices: []
+ globalNotices: [],
+ layoutHeight: 0
}
const interfaceMod = {
@@ -65,6 +66,9 @@ const interfaceMod = {
},
removeGlobalNotice (state, notice) {
state.globalNotices = state.globalNotices.filter(n => n !== notice)
+ },
+ setLayoutHeight (state, value) {
+ state.layoutHeight = value
}
},
actions: {
@@ -110,6 +114,9 @@ const interfaceMod = {
},
removeGlobalNotice ({ commit }, notice) {
commit('removeGlobalNotice', notice)
+ },
+ setLayoutHeight ({ commit }, value) {
+ commit('setLayoutHeight', value)
}
}
}
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 7fbf685c..64f5b587 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -478,7 +478,7 @@ export const mutations = {
},
setDeleted (state, { status }) {
const newStatus = state.allStatusesObject[status.id]
- newStatus.deleted = true
+ if (newStatus) newStatus.deleted = true
},
setManyDeleted (state, condition) {
Object.values(state.allStatusesObject).forEach(status => {
@@ -521,6 +521,9 @@ export const mutations = {
dismissNotification (state, { id }) {
state.notifications.data = state.notifications.data.filter(n => n.id !== id)
},
+ dismissNotifications (state, { finder }) {
+ state.notifications.data = state.notifications.data.filter(n => finder)
+ },
updateNotification (state, { id, updater }) {
const notification = find(state.notifications.data, n => n.id === id)
notification && updater(notification)
diff --git a/src/modules/users.js b/src/modules/users.js
index 7e136c61..16c1e566 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -498,6 +498,7 @@ const users = {
store.dispatch('stopFetchingFollowRequests')
store.commit('clearNotifications')
store.commit('resetStatuses')
+ store.dispatch('resetChats')
})
},
loginUser (store, accessToken) {
@@ -537,6 +538,9 @@ const users = {
// Start fetching notifications
store.dispatch('startFetchingNotifications')
+
+ // Start fetching chats
+ store.dispatch('startFetchingChats')
}
if (store.getters.mergedConfig.useStreamingApi) {
@@ -544,6 +548,7 @@ const users = {
console.error('Failed initializing MastoAPI Streaming socket', error)
startPolling()
}).then(() => {
+ store.dispatch('fetchChats', { latest: true })
setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
})
} else {
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 14e63e4f..5428cc2a 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,5 +1,5 @@
import { each, map, concat, last, get } from 'lodash'
-import { parseStatus, parseUser, parseNotification, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
+import { parseStatus, parseUser, parseNotification, parseAttachment, parseChat, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
import { RegistrationError, StatusCodeError } from '../errors/errors'
/* eslint-env browser */
@@ -81,6 +81,11 @@ const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
+const PLEROMA_CHATS_URL = `/api/v1/pleroma/chats`
+const PLEROMA_CHAT_URL = id => `/api/v1/pleroma/chats/by-account-id/${id}`
+const PLEROMA_CHAT_MESSAGES_URL = id => `/api/v1/pleroma/chats/${id}/messages`
+const PLEROMA_CHAT_READ_URL = id => `/api/v1/pleroma/chats/${id}/read`
+const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => `/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
const oldfetch = window.fetch
@@ -117,13 +122,18 @@ const promisedRequest = ({ method, url, params, payload, credentials, headers =
}
return fetch(url, options)
.then((response) => {
- return new Promise((resolve, reject) => response.json()
- .then((json) => {
- if (!response.ok) {
- return reject(new StatusCodeError(response.status, json, { url, options }, response))
- }
- return resolve(json)
- }))
+ return new Promise((resolve, reject) => {
+ response.json()
+ .then((json) => {
+ if (!response.ok) {
+ return reject(new StatusCodeError(response.status, json, { url, options }, response))
+ }
+ return resolve(json)
+ })
+ .catch((error) => {
+ return reject(new StatusCodeError(response.status, error.message, { url, options }, response))
+ })
+ })
})
}
@@ -1067,6 +1077,10 @@ const MASTODON_STREAMING_EVENTS = new Set([
'filters_changed'
])
+const PLEROMA_STREAMING_EVENTS = new Set([
+ 'pleroma:chat_update'
+])
+
// A thin wrapper around WebSocket API that allows adding a pre-processor to it
// Uses EventTarget and a CustomEvent to proxy events
export const ProcessedWS = ({
@@ -1123,7 +1137,7 @@ export const handleMastoWS = (wsEvent) => {
if (!data) return
const parsedEvent = JSON.parse(data)
const { event, payload } = parsedEvent
- if (MASTODON_STREAMING_EVENTS.has(event)) {
+ if (MASTODON_STREAMING_EVENTS.has(event) || PLEROMA_STREAMING_EVENTS.has(event)) {
// MastoBE and PleromaBE both send payload for delete as a PLAIN string
if (event === 'delete') {
return { event, id: payload }
@@ -1133,6 +1147,8 @@ export const handleMastoWS = (wsEvent) => {
return { event, status: parseStatus(data) }
} else if (event === 'notification') {
return { event, notification: parseNotification(data) }
+ } else if (event === 'pleroma:chat_update') {
+ return { event, chatUpdate: parseChat(data) }
}
} else {
console.warn('Unknown event', wsEvent)
@@ -1140,6 +1156,81 @@ export const handleMastoWS = (wsEvent) => {
}
}
+export const WSConnectionStatus = Object.freeze({
+ 'JOINED': 1,
+ 'CLOSED': 2,
+ 'ERROR': 3
+})
+
+const chats = ({ credentials }) => {
+ return fetch(PLEROMA_CHATS_URL, { headers: authHeaders(credentials) })
+ .then((data) => data.json())
+ .then((data) => {
+ return { chats: data.map(parseChat).filter(c => c) }
+ })
+}
+
+const getOrCreateChat = ({ accountId, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_CHAT_URL(accountId),
+ method: 'POST',
+ credentials
+ })
+}
+
+const chatMessages = ({ id, credentials, maxId, sinceId, limit = 20 }) => {
+ let url = PLEROMA_CHAT_MESSAGES_URL(id)
+ const args = [
+ maxId && `max_id=${maxId}`,
+ sinceId && `since_id=${sinceId}`,
+ limit && `limit=${limit}`
+ ].filter(_ => _).join('&')
+
+ url = url + (args ? '?' + args : '')
+
+ return promisedRequest({
+ url,
+ method: 'GET',
+ credentials
+ })
+}
+
+const sendChatMessage = ({ id, content, mediaId = null, credentials }) => {
+ const payload = {
+ 'content': content
+ }
+
+ if (mediaId) {
+ payload['media_id'] = mediaId
+ }
+
+ return promisedRequest({
+ url: PLEROMA_CHAT_MESSAGES_URL(id),
+ method: 'POST',
+ payload: payload,
+ credentials
+ })
+}
+
+const readChat = ({ id, lastReadId, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_CHAT_READ_URL(id),
+ method: 'POST',
+ payload: {
+ 'last_read_id': lastReadId
+ },
+ credentials
+ })
+}
+
+const deleteChatMessage = ({ chatId, messageId, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_DELETE_CHAT_MESSAGE_URL(chatId, messageId),
+ method: 'DELETE',
+ credentials
+ })
+}
+
const apiService = {
verifyCredentials,
fetchTimeline,
@@ -1218,7 +1309,13 @@ const apiService = {
fetchKnownDomains,
fetchDomainMutes,
muteDomain,
- unmuteDomain
+ unmuteDomain,
+ chats,
+ getOrCreateChat,
+ chatMessages,
+ sendChatMessage,
+ readChat,
+ deleteChatMessage
}
export default apiService
diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js
new file mode 100644
index 00000000..763a7607
--- /dev/null
+++ b/src/services/chat_service/chat_service.js
@@ -0,0 +1,150 @@
+import _ from 'lodash'
+
+const empty = (chatId) => {
+ return {
+ idIndex: {},
+ messages: [],
+ newMessageCount: 0,
+ lastSeenTimestamp: 0,
+ chatId: chatId,
+ minId: undefined,
+ lastMessage: undefined
+ }
+}
+
+const clear = (storage) => {
+ storage.idIndex = {}
+ storage.messages.splice(0, storage.messages.length)
+ storage.newMessageCount = 0
+ storage.lastSeenTimestamp = 0
+ storage.minId = undefined
+ storage.lastMessage = undefined
+}
+
+const deleteMessage = (storage, messageId) => {
+ if (!storage) { return }
+ storage.messages = storage.messages.filter(m => m.id !== messageId)
+ delete storage.idIndex[messageId]
+
+ if (storage.lastMessage && (storage.lastMessage.id === messageId)) {
+ storage.lastMessage = _.maxBy(storage.messages, 'id')
+ }
+
+ if (storage.minId === messageId) {
+ storage.minId = _.minBy(storage.messages, 'id')
+ }
+}
+
+const add = (storage, { messages: newMessages }) => {
+ if (!storage) { return }
+ for (let i = 0; i < newMessages.length; i++) {
+ const message = newMessages[i]
+
+ // sanity check
+ if (message.chat_id !== storage.chatId) { return }
+
+ if (!storage.minId || message.id < storage.minId) {
+ storage.minId = message.id
+ }
+
+ if (!storage.lastMessage || message.id > storage.lastMessage.id) {
+ storage.lastMessage = message
+ }
+
+ if (!storage.idIndex[message.id]) {
+ if (storage.lastSeenTimestamp < message.created_at) {
+ storage.newMessageCount++
+ }
+ storage.messages.push(message)
+ storage.idIndex[message.id] = message
+ }
+ }
+}
+
+const resetNewMessageCount = (storage) => {
+ if (!storage) { return }
+ storage.newMessageCount = 0
+ storage.lastSeenTimestamp = new Date()
+}
+
+// Inserts date separators and marks the head and tail if it's the chain of messages made by the same user
+const getView = (storage) => {
+ if (!storage) { return [] }
+
+ const result = []
+ const messages = _.sortBy(storage.messages, ['id', 'desc'])
+ const firstMessages = messages[0]
+ let prev = messages[messages.length - 1]
+ let currentMessageChainId
+
+ if (firstMessages) {
+ const date = new Date(firstMessages.created_at)
+ date.setHours(0, 0, 0, 0)
+ result.push({
+ type: 'date',
+ date,
+ id: date.getTime().toString()
+ })
+ }
+
+ let afterDate = false
+
+ for (let i = 0; i < messages.length; i++) {
+ const message = messages[i]
+ const nextMessage = messages[i + 1]
+
+ const date = new Date(message.created_at)
+ date.setHours(0, 0, 0, 0)
+
+ // insert date separator and start a new message chain
+ if (prev && prev.date < date) {
+ result.push({
+ type: 'date',
+ date,
+ id: date.getTime().toString()
+ })
+
+ prev['isTail'] = true
+ currentMessageChainId = undefined
+ afterDate = true
+ }
+
+ const object = {
+ type: 'message',
+ data: message,
+ date,
+ id: message.id,
+ messageChainId: currentMessageChainId
+ }
+
+ // end a message chian
+ if ((nextMessage && nextMessage.account_id) !== message.account_id) {
+ object['isTail'] = true
+ currentMessageChainId = undefined
+ }
+
+ // start a new message chain
+ if ((prev && prev.data && prev.data.account_id) !== message.account_id || afterDate) {
+ currentMessageChainId = _.uniqueId()
+ object['isHead'] = true
+ object['messageChainId'] = currentMessageChainId
+ }
+
+ result.push(object)
+ prev = object
+ afterDate = false
+ }
+
+ return result
+}
+
+const ChatService = {
+ add,
+ empty,
+ getView,
+ deleteMessage,
+ resetNewMessageCount,
+ clear
+}
+
+export default ChatService
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index ec83c02a..7ea8a16c 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -183,6 +183,7 @@ export const parseUser = (data) => {
output.deactivated = data.pleroma.deactivated
output.notification_settings = data.pleroma.notification_settings
+ output.unread_chat_count = data.pleroma.unread_chat_count
}
output.tags = output.tags || []
@@ -372,7 +373,7 @@ export const parseNotification = (data) => {
? parseStatus(data.notice.favorited_status)
: parsedNotice
output.action = parsedNotice
- output.from_profile = parseUser(data.from_profile)
+ output.from_profile = output.type === 'pleroma:chat_mention' ? parseUser(data.account) : parseUser(data.from_profile)
}
output.created_at = new Date(data.created_at)
@@ -398,3 +399,34 @@ export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
minId: flakeId ? minId : parseInt(minId, 10)
}
}
+
+export const parseChat = (chat) => {
+ const output = {}
+ output.id = chat.id
+ output.account = parseUser(chat.account)
+ output.unread = chat.unread
+ output.lastMessage = parseChatMessage(chat.last_message)
+ output.updated_at = new Date(chat.updated_at)
+ return output
+}
+
+export const parseChatMessage = (message) => {
+ if (!message) { return }
+ if (message.isNormalized) { return message }
+ const output = message
+ output.id = message.id
+ output.created_at = new Date(message.created_at)
+ output.chat_id = message.chat_id
+ if (message.content) {
+ output.content = addEmojis(message.content, message.emojis)
+ } else {
+ output.content = ''
+ }
+ if (message.attachment) {
+ output.attachments = [parseAttachment(message.attachment)]
+ } else {
+ output.attachments = []
+ }
+ output.isNormalized = true
+ return output
+}
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index fbdcf562..07425abd 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -106,7 +106,8 @@ export const generateRadii = (input) => {
avatar: 5,
avatarAlt: 50,
tooltip: 2,
- attachment: 5
+ attachment: 5,
+ chatMessage: inputRadii.panel
})
return {
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index 6b25cd6f..b58ca9be 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -23,7 +23,9 @@ export const LAYERS = {
inputTopBar: 'topBar',
alert: 'bg',
alertPanel: 'panel',
- poll: 'bg'
+ poll: 'bg',
+ chatBg: 'underlay',
+ chatMessage: 'chatBg'
}
/* By default opacity slots have 1 as default opacity
@@ -667,5 +669,54 @@ export const SLOT_INHERITANCE = {
layer: 'badge',
variant: 'badgeNotification',
textColor: 'bw'
+ },
+
+ chatBg: {
+ depends: ['bg']
+ },
+
+ chatMessage: {
+ depends: ['chatBg']
+ },
+
+ chatMessageIncomingBg: {
+ depends: ['chatMessage'],
+ layer: 'chatMessage'
+ },
+
+ chatMessageIncomingText: {
+ depends: ['text'],
+ layer: 'text'
+ },
+
+ chatMessageIncomingLink: {
+ depends: ['link'],
+ layer: 'link'
+ },
+
+ chatMessageIncomingBorder: {
+ depends: ['border'],
+ opacity: 'border',
+ color: (mod, border) => brightness(2 * mod, border).rgb
+ },
+
+ chatMessageOutgoingBg: {
+ depends: ['chatMessage'],
+ color: (mod, chatMessage) => brightness(5 * mod, chatMessage).rgb
+ },
+
+ chatMessageOutgoingText: {
+ depends: ['text'],
+ layer: 'text'
+ },
+
+ chatMessageOutgoingLink: {
+ depends: ['link'],
+ layer: 'link'
+ },
+
+ chatMessageOutgoingBorder: {
+ depends: ['chatMessage'],
+ opacity: 'chatMessage'
}
}
diff --git a/src/services/window_utils/window_utils.js b/src/services/window_utils/window_utils.js
index faff6cb9..909088db 100644
--- a/src/services/window_utils/window_utils.js
+++ b/src/services/window_utils/window_utils.js
@@ -3,3 +3,8 @@ export const windowWidth = () =>
window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth
+
+export const windowHeight = () =>
+ window.innerHeight ||
+ document.documentElement.clientHeight ||
+ document.body.clientHeight
diff --git a/static/fontello.json b/static/fontello.json
index 5ef8544e..706800cd 100644
--- a/static/fontello.json
+++ b/static/fontello.json
@@ -399,6 +399,12 @@
"css": "doc",
"code": 59433,
"src": "fontawesome"
+ },
+ {
+ "uid": "98d9c83c1ee7c2c25af784b518c522c5",
+ "css": "block",
+ "code": 59434,
+ "src": "fontawesome"
}
]
}
\ No newline at end of file
diff --git a/test/unit/specs/boot/routes.spec.js b/test/unit/specs/boot/routes.spec.js
index a415aeaf..3673256f 100644
--- a/test/unit/specs/boot/routes.spec.js
+++ b/test/unit/specs/boot/routes.spec.js
@@ -1,14 +1,22 @@
+import Vuex from 'vuex'
import routes from 'src/boot/routes'
import { createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'
const localVue = createLocalVue()
+localVue.use(Vuex)
localVue.use(VueRouter)
+const store = new Vuex.Store({
+ state: {
+ instance: {}
+ }
+})
+
describe('routes', () => {
const router = new VueRouter({
mode: 'abstract',
- routes: routes({})
+ routes: routes(store)
})
it('root path', () => {
From f05f832bff58034d78de9478ae2dbb06284dea75 Mon Sep 17 00:00:00 2001
From: eugenijm
Date: Sun, 21 Jun 2020 17:13:29 +0300
Subject: [PATCH 35/41] Address feedback
Use more specific css rules for the emoji dimensions in the chat list status preview.
Use more round em value for chat list item height.
Add global html overflow and height for smoother chat navigation in
the desktop Safari.
Use offsetHeight instad of a computed style when setting the window height on resize.
Remove margin-bottom from the last message to avoid occasional layout shift in the desktop Safari
Use break-word to prevent chat message text overflow
Resize and scroll the textarea when inserting a new line on ctrl+enter
Remove fade transition on route change
Ensure proper border radius at the bottom of the chat, remove unused border-radius
Prevent the chat header "jumping" on the avatar load.
---
src/App.js | 12 +--
src/App.scss | 39 ++++++-
src/App.vue | 4 +-
src/components/chat/chat.js | 63 ++++++++---
src/components/chat/chat.scss | 32 +++---
src/components/chat/chat.vue | 2 +-
src/components/chat/chat_layout.js | 100 ------------------
src/components/chat/chat_layout_utils.js | 3 +-
src/components/chat_avatar/chat_avatar.js | 23 ----
src/components/chat_avatar/chat_avatar.vue | 53 ----------
.../chat_list_item/chat_list_item.js | 4 +-
.../chat_list_item/chat_list_item.scss | 48 ++++-----
.../chat_list_item/chat_list_item.vue | 2 +-
src/components/chat_message/chat_message.js | 4 +-
src/components/chat_message/chat_message.scss | 27 ++---
src/components/chat_new/chat_new.js | 5 +-
src/components/chat_new/chat_new.scss | 2 +-
src/components/chat_title/chat_title.js | 10 +-
src/components/chat_title/chat_title.vue | 39 ++++---
src/components/emoji_input/emoji_input.js | 23 +++-
src/components/media_upload/media_upload.vue | 13 ---
.../post_status_form/post_status_form.js | 19 ++--
.../post_status_form/post_status_form.vue | 20 +++-
src/services/chat_service/chat_service.js | 19 ++--
.../chat_service/chat_service.spec.js | 89 ++++++++++++++++
25 files changed, 317 insertions(+), 338 deletions(-)
delete mode 100644 src/components/chat/chat_layout.js
delete mode 100644 src/components/chat_avatar/chat_avatar.js
delete mode 100644 src/components/chat_avatar/chat_avatar.vue
create mode 100644 test/unit/specs/services/chat_service/chat_service.spec.js
diff --git a/src/App.js b/src/App.js
index 84300e00..ded772fa 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 29ce73a8..e2e2d079 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 5d429934..0276c6a6 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 6e23c20c..9c4e5b05 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 13c52ea3..6ae7ebc9 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 d8c91dbe..62b72e14 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 07ae3abf..00000000
--- 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 f07ba2a1..609dc0c9 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 7b26e07c..00000000
--- 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 f54a7151..00000000
--- 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 1c27088c..b6b0519a 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 12269f89..3ec59ea2 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 26ad581b..640426b8 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 39216677..11305444 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 2723d5f5..e424bb1f 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 fd42d125..cfd1e6d1 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 a27da090..f0123447 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 d719eae1..c8865d77 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 90d0fa81..59e4dc26 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 d8df68d6..7454958b 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 763a7607..b60a889b 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 00000000..4e8e566b
--- /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'])
+ })
+ })
+})
From 45901c8da654bbeaae71cc484ea08f39a332baa7 Mon Sep 17 00:00:00 2001
From: eugenijm
Date: Mon, 6 Jul 2020 16:55:29 +0300
Subject: [PATCH 36/41] Disable status preview in the chat posting form
---
src/components/chat/chat.vue | 1 +
src/components/post_status_form/post_status_form.js | 1 +
src/components/post_status_form/post_status_form.vue | 5 ++++-
3 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue
index 62b72e14..2e4538c8 100644
--- a/src/components/chat/chat.vue
+++ b/src/components/chat/chat.vue
@@ -75,6 +75,7 @@
:disable-polls="true"
:disable-sensitivity-checkbox="true"
:disable-submit="errorLoadingChat || !currentChat"
+ :disable-preview="true"
:post-handler="sendMessage"
:submit-on-enter="!mobileLayout"
:preserve-focus="!mobileLayout"
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 59e4dc26..9e7cce0f 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -41,6 +41,7 @@ const PostStatusForm = {
'disablePolls',
'disableSensitivityCheckbox',
'disableSubmit',
+ 'disablePreview',
'placeholder',
'maxHeight',
'postHandler',
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 7454958b..3dcf1f79 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -70,7 +70,10 @@
{{ $t('post_status.direct_warning_to_first_only') }}
{{ $t('post_status.direct_warning_to_all') }}
-
+
-
+
+
+ {{ $t('chats.empty_chat_list_placeholder') }}
+
@@ -39,10 +48,24 @@
@import '../../_variables.scss';
.chat-list {
- min-height: calc(100vh - 67px);
+ min-height: 25em;
margin-bottom: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
+
+ &::after {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+}
+
+.emtpy-chat-list-alert {
+ padding: 3em;
+ font-size: 1.2em;
+ display: flex;
+ justify-content: center;
+ color: $fallback--text;
+ color: var(--faint, $fallback--text);
}
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 9e7cce0f..b0d94555 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -192,7 +192,7 @@ const PostStatusForm = {
this.newStatus.poll.error
},
showPreview () {
- return !!this.preview || this.previewLoading
+ return !this.disablePreview && (!!this.preview || this.previewLoading)
},
emptyStatus () {
return this.newStatus.status.trim() === '' && this.newStatus.files.length === 0
diff --git a/src/i18n/en.json b/src/i18n/en.json
index c9a34556..5cc75460 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -794,7 +794,8 @@
"more": "More",
"delete_confirm": "Do you really want to delete this message?",
"error_loading_chat": "Something went wrong when loading the chat.",
- "error_sending_message": "Something went wrong when sending the message."
+ "error_sending_message": "Something went wrong when sending the message.",
+ "empty_chat_list_placeholder": "You don't have any chats yet. Start a new chat!"
},
"file_type": {
"audio": "Audio",
diff --git a/src/modules/chats.js b/src/modules/chats.js
index f868ca0c..228d6256 100644
--- a/src/modules/chats.js
+++ b/src/modules/chats.js
@@ -83,9 +83,6 @@ const chats = {
resetChatNewMessageCount ({ commit }, value) {
commit('resetChatNewMessageCount', value)
},
- removeFromCurrentChatStatuses ({ commit }, { id }) {
- commit('removeFromCurrentChatStatuses', id)
- },
clearCurrentChat ({ rootState, commit, dispatch }, value) {
commit('setCurrentChatId', { chatId: undefined })
commit('setCurrentChatFetcher', { fetcher: undefined })
From ed7310c04b3e36f1256db296784b6240023786a1 Mon Sep 17 00:00:00 2001
From: eugenijm
Date: Tue, 7 Jul 2020 21:28:10 +0300
Subject: [PATCH 38/41] Undo the promise rejection on the json parser error in
promisedRequest
to keep the existing behavior in case some parts of the code rely on it
and to limit the overall scope of the changes.
---
src/services/api/api.service.js | 19 +++++++------------
1 file changed, 7 insertions(+), 12 deletions(-)
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 5428cc2a..40ea5bd9 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -122,18 +122,13 @@ const promisedRequest = ({ method, url, params, payload, credentials, headers =
}
return fetch(url, options)
.then((response) => {
- return new Promise((resolve, reject) => {
- response.json()
- .then((json) => {
- if (!response.ok) {
- return reject(new StatusCodeError(response.status, json, { url, options }, response))
- }
- return resolve(json)
- })
- .catch((error) => {
- return reject(new StatusCodeError(response.status, error.message, { url, options }, response))
- })
- })
+ return new Promise((resolve, reject) => response.json()
+ .then((json) => {
+ if (!response.ok) {
+ return reject(new StatusCodeError(response.status, json, { url, options }, response))
+ }
+ return resolve(json)
+ }))
})
}
From fc865d3a129a7d5eabf1490a82eefbdea07e3b47 Mon Sep 17 00:00:00 2001
From: eugenijm
Date: Tue, 7 Jul 2020 21:43:46 +0300
Subject: [PATCH 39/41] Remove direct style manipulations in favor of classes
---
src/components/chat_message/chat_message.js | 13 -------------
src/components/chat_message/chat_message.scss | 12 ++++++++++++
src/components/chat_message/chat_message.vue | 2 +-
src/components/mobile_nav/mobile_nav.js | 4 ++--
src/components/mobile_nav/mobile_nav.vue | 2 +-
5 files changed, 16 insertions(+), 17 deletions(-)
diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js
index 4d737e42..be4a7c89 100644
--- a/src/components/chat_message/chat_message.js
+++ b/src/components/chat_message/chat_message.js
@@ -60,19 +60,6 @@ const ChatMessage = {
currentUser: state => state.users.currentUser,
restrictedNicknames: state => state.instance.restrictedNicknames
}),
- ellipsisButtonWrapperStyle () {
- let res = {
- 'opacity': this.hovered || this.menuOpened ? '1' : '0'
- }
-
- if (this.isCurrentUser) {
- res.right = '0.4rem'
- } else {
- res.left = '0.4rem'
- }
-
- return res
- },
popoverMarginStyle () {
if (this.isCurrentUser) {
return {}
diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss
index 9d7b7936..240beea4 100644
--- a/src/components/chat_message/chat_message.scss
+++ b/src/components/chat_message/chat_message.scss
@@ -117,6 +117,10 @@
color: var(--chatMessageIncomingText, $fallback--text);
}
}
+
+ .chat-message-menu {
+ left: 0.4rem;
+ }
}
.outgoing {
@@ -139,6 +143,14 @@
.chat-message-inner {
align-items: flex-end;
}
+
+ .chat-message-menu {
+ right: 0.4rem;
+ }
+ }
+
+ .visible {
+ opacity: 1;
}
}
diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue
index 872ddf70..e923d694 100644
--- a/src/components/chat_message/chat_message.vue
+++ b/src/components/chat_message/chat_message.vue
@@ -39,7 +39,7 @@
>