purge shout/chat #49
55 changed files with 15 additions and 2872 deletions
13
src/App.js
13
src/App.js
|
@ -3,7 +3,6 @@ import NavPanel from './components/nav_panel/nav_panel.vue'
|
||||||
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
|
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
|
||||||
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
||||||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||||
import ShoutPanel from './components/shout_panel/shout_panel.vue'
|
|
||||||
import SettingsModal from './components/settings_modal/settings_modal.vue'
|
import SettingsModal from './components/settings_modal/settings_modal.vue'
|
||||||
import MediaModal from './components/media_modal/media_modal.vue'
|
import MediaModal from './components/media_modal/media_modal.vue'
|
||||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||||
|
@ -26,7 +25,6 @@ export default {
|
||||||
InstanceSpecificPanel,
|
InstanceSpecificPanel,
|
||||||
FeaturesPanel,
|
FeaturesPanel,
|
||||||
WhoToFollowPanel,
|
WhoToFollowPanel,
|
||||||
ShoutPanel,
|
|
||||||
MediaModal,
|
MediaModal,
|
||||||
SideDrawer,
|
SideDrawer,
|
||||||
MobilePostStatusButton,
|
MobilePostStatusButton,
|
||||||
|
@ -75,27 +73,16 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
shout () { return this.$store.state.shout.joined },
|
|
||||||
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
||||||
showInstanceSpecificPanel () {
|
showInstanceSpecificPanel () {
|
||||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||||
!this.$store.getters.mergedConfig.hideISP &&
|
!this.$store.getters.mergedConfig.hideISP &&
|
||||||
this.$store.state.instance.instanceSpecificPanelContent
|
this.$store.state.instance.instanceSpecificPanelContent
|
||||||
},
|
},
|
||||||
isChats () {
|
|
||||||
return this.$route.name === 'chat' || this.$route.name === 'chats'
|
|
||||||
},
|
|
||||||
newPostButtonShown () {
|
newPostButtonShown () {
|
||||||
if (this.isChats) return false
|
|
||||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
|
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
|
||||||
},
|
},
|
||||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||||
shoutboxPosition () {
|
|
||||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
|
|
||||||
},
|
|
||||||
hideShoutbox () {
|
|
||||||
return this.$store.getters.mergedConfig.hideShoutbox
|
|
||||||
},
|
|
||||||
layoutType () { return this.$store.state.interface.layoutType },
|
layoutType () { return this.$store.state.interface.layoutType },
|
||||||
privateMode () { return this.$store.state.instance.private },
|
privateMode () { return this.$store.state.instance.private },
|
||||||
reverseLayout () {
|
reverseLayout () {
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
<div
|
<div
|
||||||
id="main-scroller"
|
id="main-scroller"
|
||||||
class="column main"
|
class="column main"
|
||||||
:class="{ '-full-height': isChats }"
|
:class="{ '-full-height': false }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!currentUser"
|
v-if="!currentUser"
|
||||||
|
@ -55,12 +55,6 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<media-modal />
|
<media-modal />
|
||||||
<shout-panel
|
|
||||||
v-if="currentUser && shout && !hideShoutbox"
|
|
||||||
:floating="true"
|
|
||||||
class="floating-shout mobile-hidden"
|
|
||||||
:class="{ '-left': shoutboxPosition }"
|
|
||||||
/>
|
|
||||||
<MobilePostStatusButton />
|
<MobilePostStatusButton />
|
||||||
<UserReportingModal />
|
<UserReportingModal />
|
||||||
<PostStatusModal />
|
<PostStatusModal />
|
||||||
|
|
|
@ -27,7 +27,6 @@ $fallback--tooltipRadius: 5px;
|
||||||
$fallback--avatarRadius: 4px;
|
$fallback--avatarRadius: 4px;
|
||||||
$fallback--avatarAltRadius: 10px;
|
$fallback--avatarAltRadius: 10px;
|
||||||
$fallback--attachmentRadius: 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;
|
$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;
|
||||||
|
|
||||||
|
|
|
@ -247,9 +247,6 @@ const getNodeInfo = async ({ store }) => {
|
||||||
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
|
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
|
||||||
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
|
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
|
||||||
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
||||||
store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||||
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
||||||
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
||||||
|
|
|
@ -6,8 +6,6 @@ import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue
|
||||||
import ConversationPage from 'components/conversation-page/conversation-page.vue'
|
import ConversationPage from 'components/conversation-page/conversation-page.vue'
|
||||||
import Interactions from 'components/interactions/interactions.vue'
|
import Interactions from 'components/interactions/interactions.vue'
|
||||||
import DMs from 'components/dm_timeline/dm_timeline.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 UserProfile from 'components/user_profile/user_profile.vue'
|
||||||
import Search from 'components/search/search.vue'
|
import Search from 'components/search/search.vue'
|
||||||
import Registration from 'components/registration/registration.vue'
|
import Registration from 'components/registration/registration.vue'
|
||||||
|
@ -16,7 +14,6 @@ import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
||||||
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
||||||
import Notifications from 'components/notifications/notifications.vue'
|
import Notifications from 'components/notifications/notifications.vue'
|
||||||
import AuthForm from 'components/auth_form/auth_form.js'
|
import AuthForm from 'components/auth_form/auth_form.js'
|
||||||
import ShoutPanel from 'components/shout_panel/shout_panel.vue'
|
|
||||||
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
||||||
import About from 'components/about/about.vue'
|
import About from 'components/about/about.vue'
|
||||||
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
|
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
|
||||||
|
@ -68,7 +65,6 @@ export default (store) => {
|
||||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute },
|
{ name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'login', path: '/login', component: AuthForm },
|
{ name: 'login', path: '/login', component: AuthForm },
|
||||||
{ name: 'shout-panel', path: '/shout-panel', component: ShoutPanel, props: () => ({ floating: false }) },
|
|
||||||
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
||||||
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
||||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
||||||
|
@ -80,12 +76,5 @@ export default (store) => {
|
||||||
{ name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile }
|
{ name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile }
|
||||||
]
|
]
|
||||||
|
|
||||||
if (store.state.instance.pleromaChatMessagesAvailable) {
|
|
||||||
routes = routes.concat([
|
|
||||||
{ 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 }
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
return routes
|
return routes
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { mapState } from 'vuex'
|
|
||||||
import ProgressButton from '../progress_button/progress_button.vue'
|
import ProgressButton from '../progress_button/progress_button.vue'
|
||||||
import Popover from '../popover/popover.vue'
|
import Popover from '../popover/popover.vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
@ -36,18 +35,7 @@ const AccountActions = {
|
||||||
},
|
},
|
||||||
reportUser () {
|
reportUser () {
|
||||||
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
|
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
|
||||||
},
|
|
||||||
openChat () {
|
|
||||||
this.$router.push({
|
|
||||||
name: 'chat',
|
|
||||||
params: { username: this.$store.state.users.currentUser.screen_name, recipient_id: this.user.id }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState({
|
|
||||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,13 +48,6 @@
|
||||||
>
|
>
|
||||||
{{ $t('user_card.report') }}
|
{{ $t('user_card.report') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
v-if="pleromaChatMessagesAvailable"
|
|
||||||
class="btn button-default btn-block dropdown-item"
|
|
||||||
@click="openChat"
|
|
||||||
>
|
|
||||||
{{ $t('user_card.message') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:trigger>
|
<template v-slot:trigger>
|
||||||
|
|
|
@ -1,346 +0,0 @@
|
||||||
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 PostStatusForm from '../post_status_form/post_status_form.vue'
|
|
||||||
import ChatTitle from '../chat_title/chat_title.vue'
|
|
||||||
import chatService from '../../services/chat_service/chat_service.js'
|
|
||||||
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
|
|
||||||
import { getScrollPosition, getNewTopPosition, isBottomedOut, isScrollable } from './chat_layout_utils.js'
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
|
||||||
import {
|
|
||||||
faChevronDown,
|
|
||||||
faChevronLeft
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
|
|
||||||
|
|
||||||
library.add(
|
|
||||||
faChevronDown,
|
|
||||||
faChevronLeft
|
|
||||||
)
|
|
||||||
|
|
||||||
const BOTTOMED_OUT_OFFSET = 10
|
|
||||||
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 10
|
|
||||||
const SAFE_RESIZE_TIME_OFFSET = 100
|
|
||||||
const MARK_AS_READ_DELAY = 1500
|
|
||||||
const MAX_RETRIES = 10
|
|
||||||
|
|
||||||
const Chat = {
|
|
||||||
components: {
|
|
||||||
ChatMessage,
|
|
||||||
ChatTitle,
|
|
||||||
PostStatusForm
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
jumpToBottomButtonVisible: false,
|
|
||||||
hoveredMessageChainId: undefined,
|
|
||||||
lastScrollPosition: {},
|
|
||||||
scrollableContainerHeight: '100%',
|
|
||||||
errorLoadingChat: false,
|
|
||||||
messageRetriers: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.startFetching()
|
|
||||||
window.addEventListener('resize', this.handleResize)
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
window.addEventListener('scroll', this.handleScroll)
|
|
||||||
if (typeof document.hidden !== 'undefined') {
|
|
||||||
document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.handleResize()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
unmounted () {
|
|
||||||
window.removeEventListener('scroll', this.handleScroll)
|
|
||||||
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
|
|
||||||
this.$store.dispatch('clearCurrentChat')
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
recipient () {
|
|
||||||
return this.currentChat && this.currentChat.account
|
|
||||||
},
|
|
||||||
recipientId () {
|
|
||||||
return this.$route.params.recipient_id
|
|
||||||
},
|
|
||||||
formPlaceholder () {
|
|
||||||
if (this.recipient) {
|
|
||||||
return this.$t('chats.message_user', { nickname: this.recipient.screen_name_ui })
|
|
||||||
} else {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
chatViewItems () {
|
|
||||||
return chatService.getView(this.currentChatMessageService)
|
|
||||||
},
|
|
||||||
newMessageCount () {
|
|
||||||
return this.currentChatMessageService && this.currentChatMessageService.newMessageCount
|
|
||||||
},
|
|
||||||
streamingEnabled () {
|
|
||||||
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
|
||||||
},
|
|
||||||
...mapGetters([
|
|
||||||
'currentChat',
|
|
||||||
'currentChatMessageService',
|
|
||||||
'findOpenedChatByRecipientId',
|
|
||||||
'mergedConfig'
|
|
||||||
]),
|
|
||||||
...mapState({
|
|
||||||
backendInteractor: state => state.api.backendInteractor,
|
|
||||||
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus,
|
|
||||||
mobileLayout: state => state.interface.layoutType === 'mobile',
|
|
||||||
currentUser: state => state.users.currentUser
|
|
||||||
})
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
chatViewItems () {
|
|
||||||
// We don't want to scroll to the bottom on a new message when the user is viewing older messages.
|
|
||||||
// Therefore we need to know whether the scroll position was at the bottom before the DOM update.
|
|
||||||
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (bottomedOutBeforeUpdate) {
|
|
||||||
this.scrollDown()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
'$route': function () {
|
|
||||||
this.startFetching()
|
|
||||||
},
|
|
||||||
mastoUserSocketStatus (newValue) {
|
|
||||||
if (newValue === WSConnectionStatus.JOINED) {
|
|
||||||
this.fetchChat({ isFirstFetch: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
// Used to animate the avatar near the first message of the message chain when any message belonging to the chain is hovered
|
|
||||||
onMessageHover ({ isHovered, messageChainId }) {
|
|
||||||
this.hoveredMessageChainId = isHovered ? messageChainId : undefined
|
|
||||||
},
|
|
||||||
onFilesDropped () {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.handleResize()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleVisibilityChange () {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (!document.hidden && this.bottomedOut(BOTTOMED_OUT_OFFSET)) {
|
|
||||||
this.scrollDown({ forceRead: true })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
// "Sticks" scroll to bottom instead of top, helps with OSK resizing the viewport
|
|
||||||
handleResize (opts = {}) {
|
|
||||||
const { expand = false, delayed = false } = opts
|
|
||||||
|
|
||||||
if (delayed) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.handleResize({ ...opts, delayed: false })
|
|
||||||
}, SAFE_RESIZE_TIME_OFFSET)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
const { offsetHeight = undefined } = getScrollPosition()
|
|
||||||
const diff = this.lastScrollPosition.offsetHeight - offsetHeight
|
|
||||||
if (diff !== 0 || (!this.bottomedOut() && expand)) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
window.scrollTo({ top: window.scrollY + diff })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.lastScrollPosition = getScrollPosition()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
scrollDown (options = {}) {
|
|
||||||
const { behavior = 'auto', forceRead = false } = options
|
|
||||||
this.$nextTick(() => {
|
|
||||||
window.scrollTo({ top: document.documentElement.scrollHeight, behavior })
|
|
||||||
})
|
|
||||||
if (forceRead) {
|
|
||||||
this.readChat()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
readChat () {
|
|
||||||
if (!(this.currentChatMessageService && this.currentChatMessageService.maxId)) { return }
|
|
||||||
if (document.hidden) { return }
|
|
||||||
const lastReadId = this.currentChatMessageService.maxId
|
|
||||||
this.$store.dispatch('readChat', {
|
|
||||||
id: this.currentChat.id,
|
|
||||||
lastReadId
|
|
||||||
})
|
|
||||||
},
|
|
||||||
bottomedOut (offset) {
|
|
||||||
return isBottomedOut(offset)
|
|
||||||
},
|
|
||||||
reachedTop () {
|
|
||||||
return window.scrollY <= 0
|
|
||||||
},
|
|
||||||
cullOlderCheck () {
|
|
||||||
window.setTimeout(() => {
|
|
||||||
if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
|
||||||
this.$store.dispatch('cullOlderMessages', this.currentChatMessageService.chatId)
|
|
||||||
}
|
|
||||||
}, 5000)
|
|
||||||
},
|
|
||||||
handleScroll: _.throttle(function () {
|
|
||||||
if (!this.currentChat) { return }
|
|
||||||
|
|
||||||
if (this.reachedTop()) {
|
|
||||||
this.fetchChat({ maxId: this.currentChatMessageService.minId })
|
|
||||||
} else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
|
||||||
this.jumpToBottomButtonVisible = false
|
|
||||||
this.cullOlderCheck()
|
|
||||||
if (this.newMessageCount > 0) {
|
|
||||||
// Use a delay before marking as read to prevent situation where new messages
|
|
||||||
// arrive just as you're leaving the view and messages that you didn't actually
|
|
||||||
// get to see get marked as read.
|
|
||||||
window.setTimeout(() => {
|
|
||||||
// Don't mark as read if the element doesn't exist, user has left chat view
|
|
||||||
if (this.$el) this.readChat()
|
|
||||||
}, MARK_AS_READ_DELAY)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.jumpToBottomButtonVisible = true
|
|
||||||
}
|
|
||||||
}, 200),
|
|
||||||
handleScrollUp (positionBeforeLoading) {
|
|
||||||
const positionAfterLoading = getScrollPosition()
|
|
||||||
window.scrollTo({
|
|
||||||
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
fetchChat ({ isFirstFetch = false, fetchLatest = false, maxId }) {
|
|
||||||
const chatMessageService = this.currentChatMessageService
|
|
||||||
if (!chatMessageService) { return }
|
|
||||||
if (fetchLatest && this.streamingEnabled) { return }
|
|
||||||
|
|
||||||
const chatId = chatMessageService.chatId
|
|
||||||
const fetchOlderMessages = !!maxId
|
|
||||||
const sinceId = fetchLatest && chatMessageService.maxId
|
|
||||||
|
|
||||||
return this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId })
|
|
||||||
.then((messages) => {
|
|
||||||
// Clear the current chat in case we're recovering from a ws connection loss.
|
|
||||||
if (isFirstFetch) {
|
|
||||||
chatService.clear(chatMessageService)
|
|
||||||
}
|
|
||||||
|
|
||||||
const positionBeforeUpdate = getScrollPosition()
|
|
||||||
this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (fetchOlderMessages) {
|
|
||||||
this.handleScrollUp(positionBeforeUpdate)
|
|
||||||
}
|
|
||||||
|
|
||||||
// In vertical screens, the first batch of fetched messages may not always take the
|
|
||||||
// full height of the scrollable container.
|
|
||||||
// If this is the case, we want to fetch the messages until the scrollable container
|
|
||||||
// is fully populated so that the user has the ability to scroll up and load the history.
|
|
||||||
if (!isScrollable() && messages.length > 0) {
|
|
||||||
this.fetchChat({ maxId: this.currentChatMessageService.minId })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async startFetching () {
|
|
||||||
let chat = this.findOpenedChatByRecipientId(this.recipientId)
|
|
||||||
if (!chat) {
|
|
||||||
try {
|
|
||||||
chat = await this.backendInteractor.getOrCreateChat({ accountId: this.recipientId })
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error creating or getting a chat', e)
|
|
||||||
this.errorLoadingChat = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (chat) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.scrollDown({ forceRead: true })
|
|
||||||
})
|
|
||||||
this.$store.dispatch('addOpenedChat', { chat })
|
|
||||||
this.doStartFetching()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
doStartFetching () {
|
|
||||||
this.$store.dispatch('startFetchingCurrentChat', {
|
|
||||||
fetcher: () => promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000)
|
|
||||||
})
|
|
||||||
this.fetchChat({ isFirstFetch: true })
|
|
||||||
},
|
|
||||||
handleAttachmentPosting () {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.handleResize()
|
|
||||||
// When the posting form size changes because of a media attachment, we need an extra resize
|
|
||||||
// to account for the potential delay in the DOM update.
|
|
||||||
this.scrollDown({ forceRead: true })
|
|
||||||
})
|
|
||||||
},
|
|
||||||
sendMessage ({ status, media, idempotencyKey }) {
|
|
||||||
const params = {
|
|
||||||
id: this.currentChat.id,
|
|
||||||
content: status,
|
|
||||||
idempotencyKey
|
|
||||||
}
|
|
||||||
|
|
||||||
if (media[0]) {
|
|
||||||
params.mediaId = media[0].id
|
|
||||||
}
|
|
||||||
|
|
||||||
const fakeMessage = buildFakeMessage({
|
|
||||||
attachments: media,
|
|
||||||
chatId: this.currentChat.id,
|
|
||||||
content: status,
|
|
||||||
userId: this.currentUser.id,
|
|
||||||
idempotencyKey
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$store.dispatch('addChatMessages', {
|
|
||||||
chatId: this.currentChat.id,
|
|
||||||
messages: [fakeMessage]
|
|
||||||
}).then(() => {
|
|
||||||
this.handleAttachmentPosting()
|
|
||||||
})
|
|
||||||
|
|
||||||
return this.doSendMessage({ params, fakeMessage, retriesLeft: MAX_RETRIES })
|
|
||||||
},
|
|
||||||
doSendMessage ({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
|
|
||||||
if (retriesLeft <= 0) return
|
|
||||||
|
|
||||||
this.backendInteractor.sendChatMessage(params)
|
|
||||||
.then(data => {
|
|
||||||
this.$store.dispatch('addChatMessages', {
|
|
||||||
chatId: this.currentChat.id,
|
|
||||||
updateMaxId: false,
|
|
||||||
messages: [{ ...data, fakeId: fakeMessage.id }]
|
|
||||||
})
|
|
||||||
|
|
||||||
return data
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error sending message', error)
|
|
||||||
this.$store.dispatch('handleMessageError', {
|
|
||||||
chatId: this.currentChat.id,
|
|
||||||
fakeId: fakeMessage.id,
|
|
||||||
isRetry: retriesLeft !== MAX_RETRIES
|
|
||||||
})
|
|
||||||
if ((error.statusCode >= 500 && error.statusCode < 600) || error.message === 'Failed to fetch') {
|
|
||||||
this.messageRetriers[fakeMessage.id] = setTimeout(() => {
|
|
||||||
this.doSendMessage({ params, fakeMessage, retriesLeft: retriesLeft - 1 })
|
|
||||||
}, 1000 * (2 ** (MAX_RETRIES - retriesLeft)))
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
|
|
||||||
return Promise.resolve(fakeMessage)
|
|
||||||
},
|
|
||||||
goBack () {
|
|
||||||
this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Chat
|
|
|
@ -1,108 +0,0 @@
|
||||||
.chat-view {
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.chat-view-inner {
|
|
||||||
height: auto;
|
|
||||||
width: 100%;
|
|
||||||
overflow: visible;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-view-body {
|
|
||||||
box-sizing: border-box;
|
|
||||||
background-color: var(--chatBg, $fallback--bg);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
overflow: visible;
|
|
||||||
min-height: calc(100vh - var(--navbar-height));
|
|
||||||
margin: 0 0 0 0;
|
|
||||||
border-radius: 10px 10px 0 0;
|
|
||||||
border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-list {
|
|
||||||
padding: 0 0.8em;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
position: sticky;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: $fallback--bg;
|
|
||||||
background-color: var(--bg, $fallback--bg);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-view-heading {
|
|
||||||
grid-template-columns: auto minmax(50%, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.go-back-button {
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1;
|
|
||||||
height: 100%;
|
|
||||||
align-self: start;
|
|
||||||
width: var(--__panel-heading-height-inner);
|
|
||||||
}
|
|
||||||
|
|
||||||
.jump-to-bottom-button {
|
|
||||||
width: 2.5em;
|
|
||||||
height: 2.5em;
|
|
||||||
border-radius: 100%;
|
|
||||||
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: 0 1px 1px rgba(0, 0, 0, 0.3), 0 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.unread-message-count {
|
|
||||||
font-size: 0.8em;
|
|
||||||
left: 50%;
|
|
||||||
margin-top: -1rem;
|
|
||||||
padding: 0.1em;
|
|
||||||
border-radius: 50px;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-loading-error {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.error {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="chat-view">
|
|
||||||
<div class="chat-view-inner">
|
|
||||||
<div
|
|
||||||
ref="inner"
|
|
||||||
class="panel-default panel chat-view-body"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref="header"
|
|
||||||
class="panel-heading -sticky chat-view-heading"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="button-unstyled go-back-button"
|
|
||||||
@click="goBack"
|
|
||||||
>
|
|
||||||
<FAIcon
|
|
||||||
size="lg"
|
|
||||||
icon="chevron-left"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<div class="title text-center">
|
|
||||||
<ChatTitle
|
|
||||||
:user="recipient"
|
|
||||||
:with-avatar="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="message-list"
|
|
||||||
:style="{ height: scrollableContainerHeight }"
|
|
||||||
>
|
|
||||||
<template v-if="!errorLoadingChat">
|
|
||||||
<ChatMessage
|
|
||||||
v-for="chatViewItem in chatViewItems"
|
|
||||||
:key="chatViewItem.id"
|
|
||||||
:author="recipient"
|
|
||||||
:chat-view-item="chatViewItem"
|
|
||||||
:hovered-message-chain="chatViewItem.messageChainId === hoveredMessageChainId"
|
|
||||||
@hover="onMessageHover"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="chat-loading-error"
|
|
||||||
>
|
|
||||||
<div class="alert error">
|
|
||||||
{{ $t('chats.error_loading_chat') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
ref="footer"
|
|
||||||
class="panel-body footer"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="jump-to-bottom-button"
|
|
||||||
:class="{ 'visible': jumpToBottomButtonVisible }"
|
|
||||||
@click="scrollDown({ behavior: 'smooth' })"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<FAIcon icon="chevron-down" />
|
|
||||||
<div
|
|
||||||
v-if="newMessageCount"
|
|
||||||
class="badge badge-notification unread-chat-count unread-message-count"
|
|
||||||
>
|
|
||||||
{{ newMessageCount }}
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<PostStatusForm
|
|
||||||
:disable-subject="true"
|
|
||||||
:disable-scope-selector="true"
|
|
||||||
:disable-notice="true"
|
|
||||||
:disable-lock-warning="true"
|
|
||||||
:disable-polls="true"
|
|
||||||
:disable-sensitivity-checkbox="true"
|
|
||||||
:disable-submit="errorLoadingChat || !currentChat"
|
|
||||||
:disable-preview="true"
|
|
||||||
:optimistic-posting="true"
|
|
||||||
:post-handler="sendMessage"
|
|
||||||
:submit-on-enter="!mobileLayout"
|
|
||||||
:preserve-focus="!mobileLayout"
|
|
||||||
:auto-focus="!mobileLayout"
|
|
||||||
:placeholder="formPlaceholder"
|
|
||||||
:file-limit="1"
|
|
||||||
max-height="160"
|
|
||||||
emoji-picker-placement="top"
|
|
||||||
@resize="handleResize"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./chat.js"></script>
|
|
||||||
<style lang="scss">
|
|
||||||
@import '../../_variables.scss';
|
|
||||||
@import './chat.scss';
|
|
||||||
</style>
|
|
|
@ -1,24 +0,0 @@
|
||||||
// Captures a scroll position
|
|
||||||
export const getScrollPosition = () => {
|
|
||||||
return {
|
|
||||||
scrollTop: window.scrollY,
|
|
||||||
scrollHeight: document.documentElement.scrollHeight,
|
|
||||||
offsetHeight: window.innerHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 = (offset = 0) => {
|
|
||||||
const scrollHeight = window.scrollY + offset
|
|
||||||
const totalHeight = document.documentElement.scrollHeight - window.innerHeight
|
|
||||||
return totalHeight <= scrollHeight
|
|
||||||
}
|
|
||||||
// Returns whether or not the scrollbar is visible.
|
|
||||||
export const isScrollable = () => {
|
|
||||||
return document.documentElement.scrollHeight > window.innerHeight
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
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
|
|
|
@ -1,64 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-if="isNew">
|
|
||||||
<ChatNew @cancel="cancelNewChat" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="chat-list panel panel-default"
|
|
||||||
>
|
|
||||||
<div class="panel-heading -sticky">
|
|
||||||
<span class="title">
|
|
||||||
{{ $t("chats.chats") }}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
class="button-default"
|
|
||||||
@click="newChat"
|
|
||||||
>
|
|
||||||
{{ $t("chats.new") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<div
|
|
||||||
v-if="sortedChatList.length > 0"
|
|
||||||
class="timeline"
|
|
||||||
>
|
|
||||||
<List :items="sortedChatList">
|
|
||||||
<template v-slot:item="{item}">
|
|
||||||
<ChatListItem
|
|
||||||
:key="item.id"
|
|
||||||
:compact="false"
|
|
||||||
:chat="item"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</List>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="emtpy-chat-list-alert"
|
|
||||||
>
|
|
||||||
<span>{{ $t('chats.empty_chat_list_placeholder') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./chat_list.js"></script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import '../../_variables.scss';
|
|
||||||
|
|
||||||
.chat-list {
|
|
||||||
min-height: 25em;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emtpy-chat-list-alert {
|
|
||||||
padding: 3em;
|
|
||||||
font-size: 1.2em;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--faint, $fallback--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -1,69 +0,0 @@
|
||||||
import { mapState } from 'vuex'
|
|
||||||
import StatusBody from '../status_content/status_content.vue'
|
|
||||||
import fileType from 'src/services/file_type/file_type.service'
|
|
||||||
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'
|
|
||||||
|
|
||||||
const ChatListItem = {
|
|
||||||
name: 'ChatListItem',
|
|
||||||
props: [
|
|
||||||
'chat'
|
|
||||||
],
|
|
||||||
components: {
|
|
||||||
UserAvatar,
|
|
||||||
AvatarList,
|
|
||||||
Timeago,
|
|
||||||
ChatTitle,
|
|
||||||
StatusBody
|
|
||||||
},
|
|
||||||
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 message = this.chat.lastMessage
|
|
||||||
const messageEmojis = message ? message.emojis : []
|
|
||||||
const isYou = message && message.account_id === this.currentUser.id
|
|
||||||
const content = message ? (this.attachmentInfo || message.content) : ''
|
|
||||||
const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content
|
|
||||||
return {
|
|
||||||
summary: '',
|
|
||||||
emojis: messageEmojis,
|
|
||||||
raw_html: messagePreview,
|
|
||||||
text: messagePreview,
|
|
||||||
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
|
|
|
@ -1,91 +0,0 @@
|
||||||
.chat-list-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
padding: 0.75em;
|
|
||||||
height: 5em;
|
|
||||||
overflow: hidden;
|
|
||||||
box-sizing: border-box;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--selectedPost, $fallback--lightBg);
|
|
||||||
box-shadow: 0 0 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-and-account-name {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-shrink: 1;
|
|
||||||
line-height: var(--post-line-height);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-preview {
|
|
||||||
display: inline-flex;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
margin: 0.35em 0;
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--faint, $fallback--text);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--faintLink, $fallback--link);
|
|
||||||
text-decoration: none;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .animated.avatar {
|
|
||||||
canvas {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.Avatar {
|
|
||||||
border-radius: $fallback--avatarAltRadius;
|
|
||||||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-preview-body {
|
|
||||||
--emoji-size: 1.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-wrapper {
|
|
||||||
line-height: var(--post-line-height);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-preview-body {
|
|
||||||
padding-right: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="chat-list-item"
|
|
||||||
@click.capture.prevent="openChat"
|
|
||||||
>
|
|
||||||
<div class="chat-list-item-left">
|
|
||||||
<UserAvatar
|
|
||||||
:user="chat.account"
|
|
||||||
height="48px"
|
|
||||||
width="48px"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="chat-list-item-center">
|
|
||||||
<div class="heading">
|
|
||||||
<span
|
|
||||||
v-if="chat.account"
|
|
||||||
class="name-and-account-name"
|
|
||||||
>
|
|
||||||
<ChatTitle
|
|
||||||
:user="chat.account"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span class="heading-right" />
|
|
||||||
<div class="time-wrapper">
|
|
||||||
<Timeago
|
|
||||||
:time="chat.updated_at"
|
|
||||||
:auto-update="60"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="chat-preview">
|
|
||||||
<StatusBody
|
|
||||||
class="chat-preview-body"
|
|
||||||
:status="messageForStatusContent"
|
|
||||||
:single-line="true"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="chat.unread > 0"
|
|
||||||
class="badge badge-notification unread-chat-count"
|
|
||||||
>
|
|
||||||
{{ chat.unread }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./chat_list_item.js"></script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import '../../_variables.scss';
|
|
||||||
@import './chat_list_item.scss';
|
|
||||||
</style>
|
|
|
@ -1,108 +0,0 @@
|
||||||
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'
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
|
||||||
import {
|
|
||||||
faTimes,
|
|
||||||
faEllipsisH
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
|
|
||||||
library.add(
|
|
||||||
faTimes,
|
|
||||||
faEllipsisH
|
|
||||||
)
|
|
||||||
|
|
||||||
const ChatMessage = {
|
|
||||||
name: 'ChatMessage',
|
|
||||||
props: [
|
|
||||||
'author',
|
|
||||||
'edited',
|
|
||||||
'noHeading',
|
|
||||||
'chatViewItem',
|
|
||||||
'hoveredMessageChain'
|
|
||||||
],
|
|
||||||
emits: ['hover'],
|
|
||||||
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: '',
|
|
||||||
emojis: this.message.emojis,
|
|
||||||
raw_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
|
|
||||||
}),
|
|
||||||
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
|
|
|
@ -1,179 +0,0 @@
|
||||||
@import '../../_variables.scss';
|
|
||||||
|
|
||||||
.chat-message-wrapper {
|
|
||||||
|
|
||||||
&.hovered-message-chain {
|
|
||||||
.animated.Avatar {
|
|
||||||
canvas {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message-menu {
|
|
||||||
transition: opacity 0.1s;
|
|
||||||
opacity: 0;
|
|
||||||
position: absolute;
|
|
||||||
top: -0.8em;
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding-top: 0.2em;
|
|
||||||
padding-bottom: 0.2em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-icon {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover, .extra-button-popover.open & {
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--text, $fallback--text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover {
|
|
||||||
width: 12em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message {
|
|
||||||
display: flex;
|
|
||||||
padding-bottom: 0.5em;
|
|
||||||
|
|
||||||
.status-body:hover {
|
|
||||||
--_still-image-img-visibility: visible;
|
|
||||||
--_still-image-canvas-visibility: hidden;
|
|
||||||
--_still-image-label-visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-wrapper {
|
|
||||||
margin-right: 0.72em;
|
|
||||||
width: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-preview, .attachments {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message-inner {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
max-width: 80%;
|
|
||||||
min-width: 10em;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&.with-media {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.status {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
border-radius: $fallback--chatMessageRadius;
|
|
||||||
border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
|
|
||||||
display: flex;
|
|
||||||
padding: 0.75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.created-at {
|
|
||||||
position: relative;
|
|
||||||
float: right;
|
|
||||||
font-size: 0.8em;
|
|
||||||
margin: -1em 0 -0.5em 0;
|
|
||||||
font-style: italic;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.without-attachment {
|
|
||||||
.message-content {
|
|
||||||
// TODO figure out how to do it properly
|
|
||||||
.RichContent::after {
|
|
||||||
margin-right: 5.4em;
|
|
||||||
content: " ";
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pending {
|
|
||||||
.status-content.media-body, .created-at {
|
|
||||||
color: var(--faint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
.status-content.media-body, .created-at {
|
|
||||||
color: $fallback--cRed;
|
|
||||||
color: var(--badgeNotification, $fallback--cRed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.incoming {
|
|
||||||
a {
|
|
||||||
color: var(--chatMessageIncomingLink, $fallback--link);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message-menu {
|
|
||||||
left: 0.4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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-menu {
|
|
||||||
right: 0.4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.visible {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
v-if="isMessage"
|
|
||||||
class="chat-message-wrapper"
|
|
||||||
:class="{ 'hovered-message-chain': hoveredMessageChain }"
|
|
||||||
@mouseover="onHover(true)"
|
|
||||||
@mouseleave="onHover(false)"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="chat-message"
|
|
||||||
:class="[{ 'outgoing': isCurrentUser, 'incoming': !isCurrentUser }]"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="!isCurrentUser"
|
|
||||||
class="avatar-wrapper"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
v-if="chatViewItem.isHead"
|
|
||||||
:to="userProfileLink"
|
|
||||||
>
|
|
||||||
<UserAvatar
|
|
||||||
:compact="true"
|
|
||||||
:better-shadow="betterShadow"
|
|
||||||
:user="author"
|
|
||||||
/>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div class="chat-message-inner">
|
|
||||||
<div
|
|
||||||
class="status-body"
|
|
||||||
:style="{ 'min-width': message.attachment ? '80%' : '' }"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="media status"
|
|
||||||
:class="{ 'without-attachment': !hasAttachment, 'pending': chatViewItem.data.pending, 'error': chatViewItem.data.error }"
|
|
||||||
style="position: relative"
|
|
||||||
@mouseenter="hovered = true"
|
|
||||||
@mouseleave="hovered = false"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="chat-message-menu"
|
|
||||||
:class="{ 'visible': hovered || menuOpened }"
|
|
||||||
>
|
|
||||||
<Popover
|
|
||||||
trigger="click"
|
|
||||||
placement="top"
|
|
||||||
:bound-to-selector="isCurrentUser ? '' : '.scrollable-message-list'"
|
|
||||||
:bound-to="{ x: 'container' }"
|
|
||||||
:margin="popoverMarginStyle"
|
|
||||||
@show="menuOpened = true"
|
|
||||||
@close="menuOpened = false"
|
|
||||||
>
|
|
||||||
<template v-slot:content>
|
|
||||||
<div class="dropdown-menu">
|
|
||||||
<button
|
|
||||||
class="button-default dropdown-item dropdown-item-icon"
|
|
||||||
@click="deleteMessage"
|
|
||||||
>
|
|
||||||
<FAIcon icon="times" /> {{ $t("chats.delete") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-slot:trigger>
|
|
||||||
<button
|
|
||||||
class="button-default menu-icon"
|
|
||||||
:title="$t('chats.more')"
|
|
||||||
>
|
|
||||||
<FAIcon icon="ellipsis-h" />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<StatusContent
|
|
||||||
class="message-content"
|
|
||||||
:status="messageForStatusContent"
|
|
||||||
:full-content="true"
|
|
||||||
>
|
|
||||||
<template v-slot:footer>
|
|
||||||
<span
|
|
||||||
class="created-at"
|
|
||||||
>
|
|
||||||
{{ createdAt }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</StatusContent>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="chat-message-date-separator"
|
|
||||||
>
|
|
||||||
<ChatMessageDate :date="chatViewItem.date" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./chat_message.js" ></script>
|
|
||||||
<style lang="scss">
|
|
||||||
@import './chat_message.scss';
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -1,83 +0,0 @@
|
||||||
import { mapState, mapGetters } from 'vuex'
|
|
||||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
|
||||||
import {
|
|
||||||
faSearch,
|
|
||||||
faChevronLeft
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
|
|
||||||
library.add(
|
|
||||||
faSearch,
|
|
||||||
faChevronLeft
|
|
||||||
)
|
|
||||||
|
|
||||||
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 (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
|
|
|
@ -1,31 +0,0 @@
|
||||||
.chat-new {
|
|
||||||
.input-wrap {
|
|
||||||
display: flex;
|
|
||||||
margin: 0.7em 0.5em 0.7em 0.5em;
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-icon {
|
|
||||||
margin-right: 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-list {
|
|
||||||
padding-bottom: 0.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.basic-user-card:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: var(--selectedPost, $fallback--lightBg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.go-back-button {
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1;
|
|
||||||
height: 100%;
|
|
||||||
align-self: start;
|
|
||||||
width: var(--__panel-heading-height-inner);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="panel-default panel chat-new"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref="header"
|
|
||||||
class="panel-heading"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="button-unstyled go-back-button"
|
|
||||||
@click="goBack"
|
|
||||||
>
|
|
||||||
<FAIcon
|
|
||||||
size="lg"
|
|
||||||
icon="chevron-left"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="input-wrap">
|
|
||||||
<div class="input-search">
|
|
||||||
<FAIcon
|
|
||||||
class="search-icon fa-scale-110 fa-old-padding"
|
|
||||||
icon="search"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
ref="search"
|
|
||||||
v-model="query"
|
|
||||||
placeholder="Search people"
|
|
||||||
@input="onInput"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="member-list">
|
|
||||||
<div
|
|
||||||
v-for="user in availableUsers"
|
|
||||||
:key="user.id"
|
|
||||||
class="member"
|
|
||||||
>
|
|
||||||
<div @click.capture.prevent="goToChat(user)">
|
|
||||||
<BasicUserCard :user="user" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./chat_new.js"></script>
|
|
||||||
<style lang="scss">
|
|
||||||
@import '../../_variables.scss';
|
|
||||||
@import './chat_new.scss';
|
|
||||||
</style>
|
|
|
@ -1,27 +0,0 @@
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
|
||||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'ChatTitle',
|
|
||||||
components: {
|
|
||||||
UserAvatar,
|
|
||||||
RichContent
|
|
||||||
},
|
|
||||||
props: [
|
|
||||||
'user', 'withAvatar'
|
|
||||||
],
|
|
||||||
computed: {
|
|
||||||
title () {
|
|
||||||
return this.user ? this.user.screen_name_ui : ''
|
|
||||||
},
|
|
||||||
htmlTitle () {
|
|
||||||
return this.user ? this.user.name_html : ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getUserProfileLink (user) {
|
|
||||||
return generateProfileLink(user.id, user.screen_name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="chat-title"
|
|
||||||
:title="title"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
v-if="withAvatar && user"
|
|
||||||
class="avatar-container"
|
|
||||||
:to="getUserProfileLink(user)"
|
|
||||||
>
|
|
||||||
<UserAvatar
|
|
||||||
class="titlebar-avatar"
|
|
||||||
:user="user"
|
|
||||||
/>
|
|
||||||
</router-link>
|
|
||||||
<RichContent
|
|
||||||
v-if="user"
|
|
||||||
class="username"
|
|
||||||
:title="'@'+(user && user.screen_name_ui)"
|
|
||||||
:html="htmlTitle"
|
|
||||||
:emoji="user.emoji || []"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./chat_title.js"></script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import '../../_variables.scss';
|
|
||||||
|
|
||||||
.chat-title {
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
--emoji-size: 14px;
|
|
||||||
|
|
||||||
.username {
|
|
||||||
max-width: 100%;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
display: inline;
|
|
||||||
word-wrap: break-word;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-container {
|
|
||||||
align-self: center;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.titlebar-avatar {
|
|
||||||
margin-right: 0.5em;
|
|
||||||
height: 1.5em;
|
|
||||||
width: 1.5em;
|
|
||||||
border-radius: $fallback--avatarAltRadius;
|
|
||||||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
|
||||||
|
|
||||||
&.animated::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -2,9 +2,6 @@ import fileSizeFormatService from '../../services/file_size_format/file_size_for
|
||||||
|
|
||||||
const FeaturesPanel = {
|
const FeaturesPanel = {
|
||||||
computed: {
|
computed: {
|
||||||
shout: function () { return this.$store.state.instance.shoutAvailable },
|
|
||||||
pleromaChatMessages: function () { return this.$store.state.instance.pleromaChatMessagesAvailable },
|
|
||||||
gopher: function () { return this.$store.state.instance.gopherAvailable },
|
|
||||||
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
|
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
|
||||||
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
|
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
|
||||||
minimalScopesMode: function () { return this.$store.state.instance.minimalScopesMode },
|
minimalScopesMode: function () { return this.$store.state.instance.minimalScopesMode },
|
||||||
|
|
|
@ -8,15 +8,6 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body features-panel">
|
<div class="panel-body features-panel">
|
||||||
<ul>
|
<ul>
|
||||||
<li v-if="shout">
|
|
||||||
{{ $t('features_panel.shout') }}
|
|
||||||
</li>
|
|
||||||
<li v-if="pleromaChatMessages">
|
|
||||||
{{ $t('features_panel.pleroma_chat_messages') }}
|
|
||||||
</li>
|
|
||||||
<li v-if="gopher">
|
|
||||||
{{ $t('features_panel.gopher') }}
|
|
||||||
</li>
|
|
||||||
<li v-if="whoToFollow">
|
<li v-if="whoToFollow">
|
||||||
{{ $t('features_panel.who_to_follow') }}
|
{{ $t('features_panel.who_to_follow') }}
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -2,7 +2,6 @@ import SideDrawer from '../side_drawer/side_drawer.vue'
|
||||||
import Notifications from '../notifications/notifications.vue'
|
import Notifications from '../notifications/notifications.vue'
|
||||||
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
||||||
import GestureService from '../../services/gesture_service/gesture_service'
|
import GestureService from '../../services/gesture_service/gesture_service'
|
||||||
import { mapGetters } from 'vuex'
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faTimes,
|
faTimes,
|
||||||
|
@ -43,11 +42,7 @@ const MobileNav = {
|
||||||
return this.unseenNotifications.length
|
return this.unseenNotifications.length
|
||||||
},
|
},
|
||||||
hideSitename () { return this.$store.state.instance.hideSitename },
|
hideSitename () { return this.$store.state.instance.hideSitename },
|
||||||
sitename () { return this.$store.state.instance.name },
|
sitename () { return this.$store.state.instance.name }
|
||||||
isChat () {
|
|
||||||
return this.$route.name === 'chat'
|
|
||||||
},
|
|
||||||
...mapGetters(['unreadChatCount', 'unreadAnnouncementCount'])
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleMobileSidebar () {
|
toggleMobileSidebar () {
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
icon="bars"
|
icon="bars"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="unreadChatCount || unreadAnnouncementCount"
|
v-if="unreadAnnouncementCount"
|
||||||
class="alert-dot"
|
class="alert-dot"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -8,11 +8,6 @@ library.add(
|
||||||
faPen
|
faPen
|
||||||
)
|
)
|
||||||
|
|
||||||
const HIDDEN_FOR_PAGES = new Set([
|
|
||||||
'chats',
|
|
||||||
'chat'
|
|
||||||
])
|
|
||||||
|
|
||||||
const MobilePostStatusButton = {
|
const MobilePostStatusButton = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -40,8 +35,6 @@ const MobilePostStatusButton = {
|
||||||
return !!this.$store.state.users.currentUser
|
return !!this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
isHidden () {
|
isHidden () {
|
||||||
if (HIDDEN_FOR_PAGES.has(this.$route.name)) { return true }
|
|
||||||
|
|
||||||
return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
|
return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
|
||||||
},
|
},
|
||||||
isPersistent () {
|
isPersistent () {
|
||||||
|
|
|
@ -56,10 +56,9 @@ const NavPanel = {
|
||||||
currentUser: state => state.users.currentUser,
|
currentUser: state => state.users.currentUser,
|
||||||
followRequestCount: state => state.api.followRequests.length,
|
followRequestCount: state => state.api.followRequests.length,
|
||||||
privateMode: state => state.instance.private,
|
privateMode: state => state.instance.private,
|
||||||
federating: state => state.instance.federating,
|
federating: state => state.instance.federating
|
||||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
|
||||||
}),
|
}),
|
||||||
...mapGetters(['unreadChatCount', 'unreadAnnouncementCount'])
|
...mapGetters(['unreadAnnouncementCount'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,24 +49,6 @@
|
||||||
/>{{ $t("nav.interactions") }}
|
/>{{ $t("nav.interactions") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="currentUser && pleromaChatMessagesAvailable">
|
|
||||||
<router-link
|
|
||||||
class="menu-item"
|
|
||||||
:to="{ name: 'chats', params: { username: currentUser.screen_name } }"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="unreadChatCount"
|
|
||||||
class="badge badge-notification"
|
|
||||||
>
|
|
||||||
{{ unreadChatCount }}
|
|
||||||
</div>
|
|
||||||
<FAIcon
|
|
||||||
fixed-width
|
|
||||||
class="fa-scale-110"
|
|
||||||
icon="comments"
|
|
||||||
/>{{ $t("nav.chats") }}
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<li v-if="currentUser && currentUser.locked">
|
<li v-if="currentUser && currentUser.locked">
|
||||||
<router-link
|
<router-link
|
||||||
class="menu-item"
|
class="menu-item"
|
||||||
|
|
|
@ -60,7 +60,7 @@ const Notifications = {
|
||||||
return this.unseenNotifications.length
|
return this.unseenNotifications.length
|
||||||
},
|
},
|
||||||
unseenCountTitle () {
|
unseenCountTitle () {
|
||||||
return this.unseenCount + (this.unreadChatCount) + this.unreadAnnouncementCount
|
return this.unseenCount + this.unreadAnnouncementCount
|
||||||
},
|
},
|
||||||
loading () {
|
loading () {
|
||||||
return this.$store.state.statuses.notifications.loading
|
return this.$store.state.statuses.notifications.loading
|
||||||
|
@ -80,7 +80,7 @@ const Notifications = {
|
||||||
notificationsToDisplay () {
|
notificationsToDisplay () {
|
||||||
return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
|
return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
|
||||||
},
|
},
|
||||||
...mapGetters(['unreadChatCount', 'unreadAnnouncementCount'])
|
...mapGetters(['unreadAnnouncementCount'])
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
unseenCountTitle (count) {
|
unseenCountTitle (count) {
|
||||||
|
|
|
@ -76,7 +76,6 @@ const GeneralTab = {
|
||||||
return this.$store.state.instance.background &&
|
return this.$store.state.instance.background &&
|
||||||
!this.$store.state.users.currentUser.background_image
|
!this.$store.state.users.currentUser.background_image
|
||||||
},
|
},
|
||||||
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
|
|
||||||
language: {
|
language: {
|
||||||
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
|
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
|
||||||
set: function (val) {
|
set: function (val) {
|
||||||
|
|
|
@ -120,14 +120,6 @@
|
||||||
{{ $t('settings.autohide_floating_post_button') }}
|
{{ $t('settings.autohide_floating_post_button') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="instanceShoutboxPresent">
|
|
||||||
<BooleanSetting
|
|
||||||
path="hideShoutbox"
|
|
||||||
expert="1"
|
|
||||||
>
|
|
||||||
{{ $t('settings.hide_shoutbox') }}
|
|
||||||
</BooleanSetting>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
|
|
|
@ -115,8 +115,7 @@ export default {
|
||||||
avatarRadiusLocal: '',
|
avatarRadiusLocal: '',
|
||||||
avatarAltRadiusLocal: '',
|
avatarAltRadiusLocal: '',
|
||||||
attachmentRadiusLocal: '',
|
attachmentRadiusLocal: '',
|
||||||
tooltipRadiusLocal: '',
|
tooltipRadiusLocal: ''
|
||||||
chatMessageRadiusLocal: ''
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
@ -231,8 +230,7 @@ export default {
|
||||||
avatar: this.avatarRadiusLocal,
|
avatar: this.avatarRadiusLocal,
|
||||||
avatarAlt: this.avatarAltRadiusLocal,
|
avatarAlt: this.avatarAltRadiusLocal,
|
||||||
tooltip: this.tooltipRadiusLocal,
|
tooltip: this.tooltipRadiusLocal,
|
||||||
attachment: this.attachmentRadiusLocal,
|
attachment: this.attachmentRadiusLocal
|
||||||
chatMessage: this.chatMessageRadiusLocal
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
preview () {
|
preview () {
|
||||||
|
|
|
@ -748,65 +748,6 @@
|
||||||
/>
|
/>
|
||||||
<ContrastRatio :contrast="previewContrast.selectedMenuLink" />
|
<ContrastRatio :contrast="previewContrast.selectedMenuLink" />
|
||||||
</div>
|
</div>
|
||||||
<div class="color-item">
|
|
||||||
<h4>{{ $t('chats.chats') }}</h4>
|
|
||||||
<ColorInput
|
|
||||||
v-model="chatBgColorLocal"
|
|
||||||
name="chatBgColor"
|
|
||||||
:fallback="previewTheme.colors.bg"
|
|
||||||
:label="$t('settings.background')"
|
|
||||||
/>
|
|
||||||
<h5>{{ $t('settings.style.advanced_colors.chat.incoming') }}</h5>
|
|
||||||
<ColorInput
|
|
||||||
v-model="chatMessageIncomingBgColorLocal"
|
|
||||||
name="chatMessageIncomingBgColor"
|
|
||||||
:fallback="previewTheme.colors.bg"
|
|
||||||
:label="$t('settings.background')"
|
|
||||||
/>
|
|
||||||
<ColorInput
|
|
||||||
v-model="chatMessageIncomingTextColorLocal"
|
|
||||||
name="chatMessageIncomingTextColor"
|
|
||||||
:fallback="previewTheme.colors.text"
|
|
||||||
:label="$t('settings.text')"
|
|
||||||
/>
|
|
||||||
<ColorInput
|
|
||||||
v-model="chatMessageIncomingLinkColorLocal"
|
|
||||||
name="chatMessageIncomingLinkColor"
|
|
||||||
:fallback="previewTheme.colors.link"
|
|
||||||
:label="$t('settings.links')"
|
|
||||||
/>
|
|
||||||
<ColorInput
|
|
||||||
v-model="chatMessageIncomingBorderColorLocal"
|
|
||||||
name="chatMessageIncomingBorderLinkColor"
|
|
||||||
:fallback="previewTheme.colors.fg"
|
|
||||||
:label="$t('settings.style.advanced_colors.chat.border')"
|
|
||||||
/>
|
|
||||||
<h5>{{ $t('settings.style.advanced_colors.chat.outgoing') }}</h5>
|
|
||||||
<ColorInput
|
|
||||||
v-model="chatMessageOutgoingBgColorLocal"
|
|
||||||
name="chatMessageOutgoingBgColor"
|
|
||||||
:fallback="previewTheme.colors.bg"
|
|
||||||
:label="$t('settings.background')"
|
|
||||||
/>
|
|
||||||
<ColorInput
|
|
||||||
v-model="chatMessageOutgoingTextColorLocal"
|
|
||||||
name="chatMessageOutgoingTextColor"
|
|
||||||
:fallback="previewTheme.colors.text"
|
|
||||||
:label="$t('settings.text')"
|
|
||||||
/>
|
|
||||||
<ColorInput
|
|
||||||
v-model="chatMessageOutgoingLinkColorLocal"
|
|
||||||
name="chatMessageOutgoingLinkColor"
|
|
||||||
:fallback="previewTheme.colors.link"
|
|
||||||
:label="$t('settings.links')"
|
|
||||||
/>
|
|
||||||
<ColorInput
|
|
||||||
v-model="chatMessageOutgoingBorderColorLocal"
|
|
||||||
name="chatMessageOutgoingBorderLinkColor"
|
|
||||||
:fallback="previewTheme.colors.bg"
|
|
||||||
:label="$t('settings.style.advanced_colors.chat.border')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -886,14 +827,6 @@
|
||||||
max="50"
|
max="50"
|
||||||
hard-min="0"
|
hard-min="0"
|
||||||
/>
|
/>
|
||||||
<RangeInput
|
|
||||||
v-model="chatMessageRadiusLocal"
|
|
||||||
name="chatMessageRadius"
|
|
||||||
:label="$t('settings.chatMessageRadius')"
|
|
||||||
:fallback="previewTheme.radii.chatMessage || 2"
|
|
||||||
max="50"
|
|
||||||
hard-min="0"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
|
||||||
import {
|
|
||||||
faBullhorn,
|
|
||||||
faTimes
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
|
|
||||||
library.add(
|
|
||||||
faBullhorn,
|
|
||||||
faTimes
|
|
||||||
)
|
|
||||||
|
|
||||||
const shoutPanel = {
|
|
||||||
props: [ 'floating' ],
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
currentMessage: '',
|
|
||||||
channel: null,
|
|
||||||
collapsed: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
messages () {
|
|
||||||
return this.$store.state.shout.messages
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
submit (message) {
|
|
||||||
this.$store.state.shout.channel.push('new_msg', { text: message }, 10000)
|
|
||||||
this.currentMessage = ''
|
|
||||||
},
|
|
||||||
togglePanel () {
|
|
||||||
this.collapsed = !this.collapsed
|
|
||||||
},
|
|
||||||
userProfileLink (user) {
|
|
||||||
return generateProfileLink(user.id, user.username, this.$store.state.instance.restrictedNicknames)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
messages (newVal) {
|
|
||||||
const scrollEl = this.$el.querySelector('.chat-window')
|
|
||||||
if (!scrollEl) return
|
|
||||||
if (scrollEl.scrollTop + scrollEl.offsetHeight + 20 > scrollEl.scrollHeight) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (!scrollEl) return
|
|
||||||
scrollEl.scrollTop = scrollEl.scrollHeight - scrollEl.offsetHeight
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default shoutPanel
|
|
|
@ -1,156 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
v-if="!collapsed || !floating"
|
|
||||||
class="shout-panel"
|
|
||||||
>
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div
|
|
||||||
class="panel-heading timeline-heading"
|
|
||||||
:class="{ 'shout-heading': floating }"
|
|
||||||
@click.stop.prevent="togglePanel"
|
|
||||||
>
|
|
||||||
<div class="title">
|
|
||||||
{{ $t('shoutbox.title') }}
|
|
||||||
<FAIcon
|
|
||||||
v-if="floating"
|
|
||||||
icon="times"
|
|
||||||
class="close-icon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="shout-window">
|
|
||||||
<div
|
|
||||||
v-for="message in messages"
|
|
||||||
:key="message.id"
|
|
||||||
class="shout-message"
|
|
||||||
>
|
|
||||||
<span class="shout-avatar">
|
|
||||||
<img :src="message.author.avatar">
|
|
||||||
</span>
|
|
||||||
<div class="shout-content">
|
|
||||||
<router-link
|
|
||||||
class="shout-name"
|
|
||||||
:to="userProfileLink(message.author)"
|
|
||||||
>
|
|
||||||
{{ message.author.username }}
|
|
||||||
</router-link>
|
|
||||||
<br>
|
|
||||||
<span class="shout-text">
|
|
||||||
{{ message.text }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="shout-input">
|
|
||||||
<textarea
|
|
||||||
v-model="currentMessage"
|
|
||||||
class="shout-input-textarea"
|
|
||||||
rows="1"
|
|
||||||
@keyup.enter="submit(currentMessage)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="shout-panel"
|
|
||||||
>
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div
|
|
||||||
class="panel-heading -stub timeline-heading shout-heading"
|
|
||||||
@click.stop.prevent="togglePanel"
|
|
||||||
>
|
|
||||||
<div class="title">
|
|
||||||
<FAIcon
|
|
||||||
class="icon"
|
|
||||||
icon="bullhorn"
|
|
||||||
/>
|
|
||||||
{{ $t('shoutbox.title') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./shout_panel.js"></script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import '../../_variables.scss';
|
|
||||||
|
|
||||||
.floating-shout {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0.5em;
|
|
||||||
z-index: 1000;
|
|
||||||
max-width: 25em;
|
|
||||||
|
|
||||||
&.-left {
|
|
||||||
left: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.-left) {
|
|
||||||
right: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.shout-panel {
|
|
||||||
.shout-heading {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--panelText, $fallback--text);
|
|
||||||
margin-right: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.shout-window {
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
max-height: 20em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shout-window-container {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shout-message {
|
|
||||||
display: flex;
|
|
||||||
padding: 0.2em 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shout-avatar {
|
|
||||||
img {
|
|
||||||
height: 24px;
|
|
||||||
width: 24px;
|
|
||||||
border-radius: $fallback--avatarRadius;
|
|
||||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
|
||||||
margin-right: 0.5em;
|
|
||||||
margin-top: 0.25em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.shout-input {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
flex: 1;
|
|
||||||
margin: 0.6em;
|
|
||||||
min-height: 3.5em;
|
|
||||||
resize: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.shout-panel {
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { mapState, mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import UserCard from '../user_card/user_card.vue'
|
import UserCard from '../user_card/user_card.vue'
|
||||||
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
||||||
import GestureService from '../../services/gesture_service/gesture_service'
|
import GestureService from '../../services/gesture_service/gesture_service'
|
||||||
|
@ -51,7 +51,6 @@ const SideDrawer = {
|
||||||
currentUser () {
|
currentUser () {
|
||||||
return this.$store.state.users.currentUser
|
return this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
shout () { return this.$store.state.shout.joined },
|
|
||||||
unseenNotifications () {
|
unseenNotifications () {
|
||||||
return unseenNotificationsFromStore(this.$store)
|
return unseenNotificationsFromStore(this.$store)
|
||||||
},
|
},
|
||||||
|
@ -85,10 +84,7 @@ const SideDrawer = {
|
||||||
}
|
}
|
||||||
return this.currentUser ? 'friends' : 'public-timeline'
|
return this.currentUser ? 'friends' : 'public-timeline'
|
||||||
},
|
},
|
||||||
...mapState({
|
...mapGetters(['unreadAnnouncementCount'])
|
||||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
|
||||||
}),
|
|
||||||
...mapGetters(['unreadChatCount', 'unreadAnnouncementCount'])
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleDrawer () {
|
toggleDrawer () {
|
||||||
|
|
|
@ -67,27 +67,6 @@
|
||||||
/> {{ $t("nav.lists") }}
|
/> {{ $t("nav.lists") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li
|
|
||||||
v-if="currentUser && pleromaChatMessagesAvailable"
|
|
||||||
@click="toggleDrawer"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'chats', params: { username: currentUser.screen_name } }"
|
|
||||||
style="position: relative"
|
|
||||||
>
|
|
||||||
<FAIcon
|
|
||||||
fixed-width
|
|
||||||
class="fa-scale-110 fa-old-padding"
|
|
||||||
icon="comments"
|
|
||||||
/> {{ $t("nav.chats") }}
|
|
||||||
<span
|
|
||||||
v-if="unreadChatCount"
|
|
||||||
class="badge badge-notification"
|
|
||||||
>
|
|
||||||
{{ unreadChatCount }}
|
|
||||||
</span>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<ul v-if="currentUser">
|
<ul v-if="currentUser">
|
||||||
<li @click="toggleDrawer">
|
<li @click="toggleDrawer">
|
||||||
|
@ -117,18 +96,6 @@
|
||||||
</span>
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li
|
|
||||||
v-if="shout"
|
|
||||||
@click="toggleDrawer"
|
|
||||||
>
|
|
||||||
<router-link :to="{ name: 'shout-panel' }">
|
|
||||||
<FAIcon
|
|
||||||
fixed-width
|
|
||||||
class="fa-scale-110 fa-old-padding"
|
|
||||||
icon="bullhorn"
|
|
||||||
/> {{ $t("shoutbox.title") }}
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
|
|
|
@ -11,7 +11,6 @@ import usersModule from './modules/users.js'
|
||||||
import apiModule from './modules/api.js'
|
import apiModule from './modules/api.js'
|
||||||
import configModule from './modules/config.js'
|
import configModule from './modules/config.js'
|
||||||
import serverSideConfigModule from './modules/serverSideConfig.js'
|
import serverSideConfigModule from './modules/serverSideConfig.js'
|
||||||
import shoutModule from './modules/shout.js'
|
|
||||||
import oauthModule from './modules/oauth.js'
|
import oauthModule from './modules/oauth.js'
|
||||||
import authFlowModule from './modules/auth_flow.js'
|
import authFlowModule from './modules/auth_flow.js'
|
||||||
import mediaViewerModule from './modules/media_viewer.js'
|
import mediaViewerModule from './modules/media_viewer.js'
|
||||||
|
@ -19,7 +18,6 @@ import oauthTokensModule from './modules/oauth_tokens.js'
|
||||||
import reportsModule from './modules/reports.js'
|
import reportsModule from './modules/reports.js'
|
||||||
import pollsModule from './modules/polls.js'
|
import pollsModule from './modules/polls.js'
|
||||||
import postStatusModule from './modules/postStatus.js'
|
import postStatusModule from './modules/postStatus.js'
|
||||||
import chatsModule from './modules/chats.js'
|
|
||||||
import announcementsModule from './modules/announcements.js'
|
import announcementsModule from './modules/announcements.js'
|
||||||
|
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
|
@ -76,7 +74,6 @@ const persistedStateOptions = {
|
||||||
api: apiModule,
|
api: apiModule,
|
||||||
config: configModule,
|
config: configModule,
|
||||||
serverSideConfig: serverSideConfigModule,
|
serverSideConfig: serverSideConfigModule,
|
||||||
shout: shoutModule,
|
|
||||||
oauth: oauthModule,
|
oauth: oauthModule,
|
||||||
authFlow: authFlowModule,
|
authFlow: authFlowModule,
|
||||||
mediaViewer: mediaViewerModule,
|
mediaViewer: mediaViewerModule,
|
||||||
|
@ -84,7 +81,6 @@ const persistedStateOptions = {
|
||||||
reports: reportsModule,
|
reports: reportsModule,
|
||||||
polls: pollsModule,
|
polls: pollsModule,
|
||||||
postStatus: postStatusModule,
|
postStatus: postStatusModule,
|
||||||
chats: chatsModule,
|
|
||||||
announcements: announcementsModule
|
announcements: announcementsModule
|
||||||
},
|
},
|
||||||
plugins,
|
plugins,
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
import { WSConnectionStatus } from '../services/api/api.service.js'
|
import { WSConnectionStatus } from '../services/api/api.service.js'
|
||||||
import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
|
|
||||||
import { Socket } from 'phoenix'
|
|
||||||
|
|
||||||
const retryTimeout = (multiplier) => 1000 * multiplier
|
const retryTimeout = (multiplier) => 1000 * multiplier
|
||||||
|
|
||||||
|
@ -102,19 +100,6 @@ const api = {
|
||||||
})
|
})
|
||||||
} else if (message.event === 'delete') {
|
} else if (message.event === 'delete') {
|
||||||
dispatch('deleteStatusById', message.id)
|
dispatch('deleteStatusById', message.id)
|
||||||
} else if (message.event === 'pleroma:chat_update') {
|
|
||||||
// The setTimeout wrapper is a temporary band-aid to avoid duplicates for the user's own messages when doing optimistic sending.
|
|
||||||
// The cause of the duplicates is the WS event arriving earlier than the HTTP response.
|
|
||||||
// This setTimeout wrapper can be removed once the commit `8e41baff` is in the stable Pleroma release.
|
|
||||||
// (`8e41baff` adds the idempotency key to the chat message entity, which PleromaFE uses when it's available, and it makes this artificial delay unnecessary).
|
|
||||||
setTimeout(() => {
|
|
||||||
dispatch('addChatMessages', {
|
|
||||||
chatId: message.chatUpdate.id,
|
|
||||||
messages: [message.chatUpdate.lastMessage]
|
|
||||||
})
|
|
||||||
dispatch('updateChat', { chat: message.chatUpdate })
|
|
||||||
maybeShowChatNotification(store, message.chatUpdate)
|
|
||||||
}, 100)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -134,15 +119,12 @@ const api = {
|
||||||
]).has(state.mastoUserSocketStatus)) {
|
]).has(state.mastoUserSocketStatus)) {
|
||||||
dispatch('stopFetchingTimeline', { timeline: 'friends' })
|
dispatch('stopFetchingTimeline', { timeline: 'friends' })
|
||||||
dispatch('stopFetchingNotifications')
|
dispatch('stopFetchingNotifications')
|
||||||
dispatch('stopFetchingChats')
|
|
||||||
}
|
}
|
||||||
commit('resetRetryMultiplier')
|
commit('resetRetryMultiplier')
|
||||||
commit('setMastoUserSocketStatus', WSConnectionStatus.JOINED)
|
commit('setMastoUserSocketStatus', WSConnectionStatus.JOINED)
|
||||||
})
|
})
|
||||||
state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
|
state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
|
||||||
console.error('Error in MastoAPI websocket:', error)
|
console.error('Error in MastoAPI websocket:', error)
|
||||||
// TODO is this needed?
|
|
||||||
dispatch('clearOpenedChats')
|
|
||||||
})
|
})
|
||||||
state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
|
state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
|
||||||
const ignoreCodes = new Set([
|
const ignoreCodes = new Set([
|
||||||
|
@ -162,7 +144,6 @@ const api = {
|
||||||
if (state.mastoUserSocketStatus !== WSConnectionStatus.ERROR) {
|
if (state.mastoUserSocketStatus !== WSConnectionStatus.ERROR) {
|
||||||
dispatch('startFetchingTimeline', { timeline: 'friends' })
|
dispatch('startFetchingTimeline', { timeline: 'friends' })
|
||||||
dispatch('startFetchingNotifications')
|
dispatch('startFetchingNotifications')
|
||||||
dispatch('startFetchingChats')
|
|
||||||
dispatch('startFetchingAnnouncements')
|
dispatch('startFetchingAnnouncements')
|
||||||
dispatch('pushGlobalNotice', {
|
dispatch('pushGlobalNotice', {
|
||||||
level: 'error',
|
level: 'error',
|
||||||
|
@ -173,7 +154,6 @@ const api = {
|
||||||
}
|
}
|
||||||
commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR)
|
commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR)
|
||||||
}
|
}
|
||||||
dispatch('clearOpenedChats')
|
|
||||||
})
|
})
|
||||||
resolve()
|
resolve()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -184,7 +164,6 @@ const api = {
|
||||||
stopMastoUserSocket ({ state, dispatch }) {
|
stopMastoUserSocket ({ state, dispatch }) {
|
||||||
dispatch('startFetchingTimeline', { timeline: 'friends' })
|
dispatch('startFetchingTimeline', { timeline: 'friends' })
|
||||||
dispatch('startFetchingNotifications')
|
dispatch('startFetchingNotifications')
|
||||||
dispatch('startFetchingChats')
|
|
||||||
state.mastoUserSocket.close()
|
state.mastoUserSocket.close()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -278,17 +257,6 @@ const api = {
|
||||||
setWsToken (store, token) {
|
setWsToken (store, token) {
|
||||||
store.commit('setWsToken', token)
|
store.commit('setWsToken', token)
|
||||||
},
|
},
|
||||||
initializeSocket ({ dispatch, commit, state, rootState }) {
|
|
||||||
// Set up websocket connection
|
|
||||||
const token = state.wsToken
|
|
||||||
if (rootState.instance.shoutAvailable && typeof token !== 'undefined' && state.socket === null) {
|
|
||||||
const socket = new Socket('/socket', { params: { token } })
|
|
||||||
socket.connect()
|
|
||||||
|
|
||||||
commit('setSocket', socket)
|
|
||||||
dispatch('initializeShout', socket)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
disconnectFromSocket ({ commit, state }) {
|
disconnectFromSocket ({ commit, state }) {
|
||||||
state.socket && state.socket.disconnect()
|
state.socket && state.socket.disconnect()
|
||||||
commit('setSocket', null)
|
commit('setSocket', null)
|
||||||
|
|
|
@ -1,240 +0,0 @@
|
||||||
import { reactive } 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'
|
|
||||||
import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
|
|
||||||
import { promiseInterval } from '../services/promise_interval/promise_interval.js'
|
|
||||||
|
|
||||||
const emptyChatList = () => ({
|
|
||||||
data: [],
|
|
||||||
idStore: {}
|
|
||||||
})
|
|
||||||
|
|
||||||
const defaultState = {
|
|
||||||
chatList: emptyChatList(),
|
|
||||||
chatListFetcher: null,
|
|
||||||
openedChats: reactive({}),
|
|
||||||
openedChatMessageServices: reactive({}),
|
|
||||||
fetcher: undefined,
|
|
||||||
currentChatId: null,
|
|
||||||
lastReadMessageId: 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: () => promiseInterval(fetcher, 60000)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
stopFetchingChats ({ commit }) {
|
|
||||||
commit('setChatListFetcher', { fetcher: undefined })
|
|
||||||
},
|
|
||||||
fetchChats ({ dispatch, rootState, commit }, params = {}) {
|
|
||||||
return rootState.api.backendInteractor.chats()
|
|
||||||
.then(({ chats }) => {
|
|
||||||
dispatch('addNewChats', { chats })
|
|
||||||
return chats
|
|
||||||
})
|
|
||||||
},
|
|
||||||
addNewChats (store, { chats }) {
|
|
||||||
const { commit, dispatch, rootGetters } = store
|
|
||||||
const newChatMessageSideEffects = (chat) => {
|
|
||||||
maybeShowChatNotification(store, chat)
|
|
||||||
}
|
|
||||||
commit('addNewChats', { dispatch, chats, rootGetters, newChatMessageSideEffects })
|
|
||||||
},
|
|
||||||
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)
|
|
||||||
},
|
|
||||||
clearCurrentChat ({ rootState, commit, dispatch }, value) {
|
|
||||||
commit('setCurrentChatId', { chatId: undefined })
|
|
||||||
commit('setCurrentChatFetcher', { fetcher: undefined })
|
|
||||||
},
|
|
||||||
readChat ({ rootState, commit, dispatch }, { id, lastReadId }) {
|
|
||||||
const isNewMessage = rootState.chats.lastReadMessageId !== lastReadId
|
|
||||||
|
|
||||||
dispatch('resetChatNewMessageCount')
|
|
||||||
commit('readChat', { id, lastReadId })
|
|
||||||
|
|
||||||
if (isNewMessage) {
|
|
||||||
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 })
|
|
||||||
},
|
|
||||||
handleMessageError ({ commit }, value) {
|
|
||||||
commit('handleMessageError', { commit, ...value })
|
|
||||||
},
|
|
||||||
cullOlderMessages ({ commit }, chatId) {
|
|
||||||
commit('cullOlderMessages', chatId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
setChatListFetcher (state, { commit, fetcher }) {
|
|
||||||
const prevFetcher = state.chatListFetcher
|
|
||||||
if (prevFetcher) {
|
|
||||||
prevFetcher.stop()
|
|
||||||
}
|
|
||||||
state.chatListFetcher = fetcher && fetcher()
|
|
||||||
},
|
|
||||||
setCurrentChatFetcher (state, { fetcher }) {
|
|
||||||
const prevFetcher = state.fetcher
|
|
||||||
if (prevFetcher) {
|
|
||||||
prevFetcher.stop()
|
|
||||||
}
|
|
||||||
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, { chats, newChatMessageSideEffects }) {
|
|
||||||
chats.forEach((updatedChat) => {
|
|
||||||
const chat = getChatById(state, updatedChat.id)
|
|
||||||
|
|
||||||
if (chat) {
|
|
||||||
const isNewMessage = (chat.lastMessage && chat.lastMessage.id) !== (updatedChat.lastMessage && updatedChat.lastMessage.id)
|
|
||||||
chat.lastMessage = updatedChat.lastMessage
|
|
||||||
chat.unread = updatedChat.unread
|
|
||||||
chat.updated_at = updatedChat.updated_at
|
|
||||||
if (isNewMessage && chat.unread) {
|
|
||||||
newChatMessageSideEffects(updatedChat)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
state.chatList.data.push(updatedChat)
|
|
||||||
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) }
|
|
||||||
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])
|
|
||||||
delete state.openedChats[chatId]
|
|
||||||
delete state.openedChatMessageServices[chatId]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setChatsLoading (state, { value }) {
|
|
||||||
state.chats.loading = value
|
|
||||||
},
|
|
||||||
addChatMessages (state, { chatId, messages, updateMaxId }) {
|
|
||||||
const chatMessageService = state.openedChatMessageServices[chatId]
|
|
||||||
if (chatMessageService) {
|
|
||||||
chatService.add(chatMessageService, { messages: messages.map(parseChatMessage), updateMaxId })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deleteChatMessage (state, { chatId, messageId }) {
|
|
||||||
const chatMessageService = state.openedChatMessageServices[chatId]
|
|
||||||
if (chatMessageService) {
|
|
||||||
chatService.deleteMessage(chatMessageService, messageId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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])
|
|
||||||
delete state.openedChats[chatId]
|
|
||||||
delete state.openedChatMessageServices[chatId]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
readChat (state, { id, lastReadId }) {
|
|
||||||
state.lastReadMessageId = lastReadId
|
|
||||||
const chat = getChatById(state, id)
|
|
||||||
if (chat) {
|
|
||||||
chat.unread = 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleMessageError (state, { chatId, fakeId, isRetry }) {
|
|
||||||
const chatMessageService = state.openedChatMessageServices[chatId]
|
|
||||||
chatService.handleMessageError(chatMessageService, fakeId, isRetry)
|
|
||||||
},
|
|
||||||
cullOlderMessages (state, chatId) {
|
|
||||||
chatService.cullOlderMessages(state.openedChatMessageServices[chatId])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default chats
|
|
|
@ -28,7 +28,6 @@ export const defaultState = {
|
||||||
customThemeSource: undefined,
|
customThemeSource: undefined,
|
||||||
hideISP: false,
|
hideISP: false,
|
||||||
hideInstanceWallpaper: false,
|
hideInstanceWallpaper: false,
|
||||||
hideShoutbox: false,
|
|
||||||
// bad name: actually hides posts of muted USERS
|
// bad name: actually hides posts of muted USERS
|
||||||
hideMutedPosts: undefined, // instance default
|
hideMutedPosts: undefined, // instance default
|
||||||
hideMutedThreads: undefined, // instance default
|
hideMutedThreads: undefined, // instance default
|
||||||
|
@ -59,7 +58,6 @@ export const defaultState = {
|
||||||
moves: true,
|
moves: true,
|
||||||
emojiReactions: true,
|
emojiReactions: true,
|
||||||
followRequest: true,
|
followRequest: true,
|
||||||
chatMention: true,
|
|
||||||
polls: true
|
polls: true
|
||||||
},
|
},
|
||||||
webPushNotifications: false,
|
webPushNotifications: false,
|
||||||
|
|
|
@ -74,9 +74,6 @@ const defaultState = {
|
||||||
knownDomains: [],
|
knownDomains: [],
|
||||||
|
|
||||||
// Feature-set, apparently, not everything here is reported...
|
// Feature-set, apparently, not everything here is reported...
|
||||||
shoutAvailable: false,
|
|
||||||
pleromaChatMessagesAvailable: false,
|
|
||||||
gopherAvailable: false,
|
|
||||||
mediaProxyAvailable: false,
|
mediaProxyAvailable: false,
|
||||||
suggestionsEnabled: false,
|
suggestionsEnabled: false,
|
||||||
suggestionsWeb: '',
|
suggestionsWeb: '',
|
||||||
|
@ -127,11 +124,6 @@ const instance = {
|
||||||
case 'name':
|
case 'name':
|
||||||
dispatch('setPageTitle')
|
dispatch('setPageTitle')
|
||||||
break
|
break
|
||||||
case 'shoutAvailable':
|
|
||||||
if (value) {
|
|
||||||
dispatch('initializeSocket')
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'theme':
|
case 'theme':
|
||||||
dispatch('setTheme', value)
|
dispatch('setTheme', value)
|
||||||
break
|
break
|
||||||
|
|
|
@ -47,10 +47,6 @@ export const settingsMap = {
|
||||||
},
|
},
|
||||||
// Privacy
|
// Privacy
|
||||||
'locked': 'locked',
|
'locked': 'locked',
|
||||||
'acceptChatMessages': {
|
|
||||||
get: 'pleroma.accepts_chat_messages',
|
|
||||||
set: 'accepts_chat_messages'
|
|
||||||
},
|
|
||||||
'allowFollowingMove': {
|
'allowFollowingMove': {
|
||||||
get: 'pleroma.allow_following_move',
|
get: 'pleroma.allow_following_move',
|
||||||
set: 'allow_following_move'
|
set: 'allow_following_move'
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
const shout = {
|
|
||||||
state: {
|
|
||||||
messages: [],
|
|
||||||
channel: { state: '' },
|
|
||||||
joined: false
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
setChannel (state, channel) {
|
|
||||||
state.channel = channel
|
|
||||||
},
|
|
||||||
addMessage (state, message) {
|
|
||||||
state.messages.push(message)
|
|
||||||
state.messages = state.messages.slice(-19, 20)
|
|
||||||
},
|
|
||||||
setMessages (state, messages) {
|
|
||||||
state.messages = messages.slice(-19, 20)
|
|
||||||
},
|
|
||||||
setJoined (state, joined) {
|
|
||||||
state.joined = joined
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
initializeShout (store, socket) {
|
|
||||||
const channel = socket.channel('chat:public')
|
|
||||||
channel.joinPush.receive('ok', () => {
|
|
||||||
store.commit('setJoined', true)
|
|
||||||
})
|
|
||||||
channel.onClose(() => {
|
|
||||||
store.commit('setJoined', false)
|
|
||||||
})
|
|
||||||
channel.onError(() => {
|
|
||||||
store.commit('setJoined', false)
|
|
||||||
})
|
|
||||||
channel.on('new_msg', (msg) => {
|
|
||||||
store.commit('addMessage', msg)
|
|
||||||
})
|
|
||||||
channel.on('messages', ({ messages }) => {
|
|
||||||
store.commit('setMessages', messages)
|
|
||||||
})
|
|
||||||
channel.join()
|
|
||||||
store.commit('setChannel', channel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default shout
|
|
|
@ -523,7 +523,6 @@ const users = {
|
||||||
store.dispatch('stopFetchingFollowRequests')
|
store.dispatch('stopFetchingFollowRequests')
|
||||||
store.commit('clearNotifications')
|
store.commit('clearNotifications')
|
||||||
store.commit('resetStatuses')
|
store.commit('resetStatuses')
|
||||||
store.dispatch('resetChats')
|
|
||||||
store.dispatch('setLastTimeline', 'public-timeline')
|
store.dispatch('setLastTimeline', 'public-timeline')
|
||||||
store.dispatch('setLayoutWidth', windowWidth())
|
store.dispatch('setLayoutWidth', windowWidth())
|
||||||
store.dispatch('setLayoutHeight', windowHeight())
|
store.dispatch('setLayoutHeight', windowHeight())
|
||||||
|
@ -555,9 +554,6 @@ const users = {
|
||||||
|
|
||||||
if (user.token) {
|
if (user.token) {
|
||||||
store.dispatch('setWsToken', user.token)
|
store.dispatch('setWsToken', user.token)
|
||||||
|
|
||||||
// Initialize the shout socket.
|
|
||||||
store.dispatch('initializeSocket')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const startPolling = () => {
|
const startPolling = () => {
|
||||||
|
@ -566,9 +562,6 @@ const users = {
|
||||||
|
|
||||||
// Start fetching notifications
|
// Start fetching notifications
|
||||||
store.dispatch('startFetchingNotifications')
|
store.dispatch('startFetchingNotifications')
|
||||||
|
|
||||||
// Start fetching chats
|
|
||||||
store.dispatch('startFetchingChats')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (store.getters.mergedConfig.useStreamingApi) {
|
if (store.getters.mergedConfig.useStreamingApi) {
|
||||||
|
@ -577,7 +570,6 @@ const users = {
|
||||||
store.dispatch('enableMastoSockets', true).catch((error) => {
|
store.dispatch('enableMastoSockets', true).catch((error) => {
|
||||||
console.error('Failed initializing MastoAPI Streaming socket', error)
|
console.error('Failed initializing MastoAPI Streaming socket', error)
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
store.dispatch('fetchChats', { latest: true })
|
|
||||||
setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
|
setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { each, map, concat, last, get } from 'lodash'
|
import { each, map, concat, last, get } from 'lodash'
|
||||||
import { parseStatus, parseUser, parseNotification, parseAttachment, parseChat, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
|
import { parseStatus, parseUser, parseNotification, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
|
||||||
import { RegistrationError, StatusCodeError } from '../errors/errors'
|
import { RegistrationError, StatusCodeError } from '../errors/errors'
|
||||||
|
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
|
@ -92,11 +92,6 @@ const MASTODON_ANNOUNCEMENTS_DISMISS_URL = id => `/api/v1/announcements/${id}/di
|
||||||
const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
|
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_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_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 PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
|
const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
|
||||||
const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements'
|
const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements'
|
||||||
const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
|
const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
|
||||||
|
@ -1378,7 +1373,6 @@ const MASTODON_STREAMING_EVENTS = new Set([
|
||||||
])
|
])
|
||||||
|
|
||||||
const PLEROMA_STREAMING_EVENTS = new Set([
|
const PLEROMA_STREAMING_EVENTS = new Set([
|
||||||
'pleroma:chat_update'
|
|
||||||
])
|
])
|
||||||
|
|
||||||
// A thin wrapper around WebSocket API that allows adding a pre-processor to it
|
// A thin wrapper around WebSocket API that allows adding a pre-processor to it
|
||||||
|
@ -1448,8 +1442,6 @@ export const handleMastoWS = (wsEvent) => {
|
||||||
return { event, status: parseStatus(data) }
|
return { event, status: parseStatus(data) }
|
||||||
} else if (event === 'notification') {
|
} else if (event === 'notification') {
|
||||||
return { event, notification: parseNotification(data) }
|
return { event, notification: parseNotification(data) }
|
||||||
} else if (event === 'pleroma:chat_update') {
|
|
||||||
return { event, chatUpdate: parseChat(data) }
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('Unknown event', wsEvent)
|
console.warn('Unknown event', wsEvent)
|
||||||
|
@ -1466,82 +1458,6 @@ export const WSConnectionStatus = Object.freeze({
|
||||||
'STARTING_INITIAL': 6
|
'STARTING_INITIAL': 6
|
||||||
})
|
})
|
||||||
|
|
||||||
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, idempotencyKey, credentials }) => {
|
|
||||||
const payload = {
|
|
||||||
'content': content
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaId) {
|
|
||||||
payload['media_id'] = mediaId
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = {}
|
|
||||||
|
|
||||||
if (idempotencyKey) {
|
|
||||||
headers['idempotency-key'] = idempotencyKey
|
|
||||||
}
|
|
||||||
|
|
||||||
return promisedRequest({
|
|
||||||
url: PLEROMA_CHAT_MESSAGES_URL(id),
|
|
||||||
method: 'POST',
|
|
||||||
payload: payload,
|
|
||||||
credentials,
|
|
||||||
headers
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = {
|
const apiService = {
|
||||||
verifyCredentials,
|
verifyCredentials,
|
||||||
fetchTimeline,
|
fetchTimeline,
|
||||||
|
@ -1639,12 +1555,6 @@ const apiService = {
|
||||||
fetchDomainMutes,
|
fetchDomainMutes,
|
||||||
muteDomain,
|
muteDomain,
|
||||||
unmuteDomain,
|
unmuteDomain,
|
||||||
chats,
|
|
||||||
getOrCreateChat,
|
|
||||||
chatMessages,
|
|
||||||
sendChatMessage,
|
|
||||||
readChat,
|
|
||||||
deleteChatMessage,
|
|
||||||
fetchAnnouncements,
|
fetchAnnouncements,
|
||||||
dismissAnnouncement,
|
dismissAnnouncement,
|
||||||
postAnnouncement,
|
postAnnouncement,
|
||||||
|
|
|
@ -1,226 +0,0 @@
|
||||||
import _ from 'lodash'
|
|
||||||
|
|
||||||
const empty = (chatId) => {
|
|
||||||
return {
|
|
||||||
idIndex: {},
|
|
||||||
idempotencyKeyIndex: {},
|
|
||||||
messages: [],
|
|
||||||
newMessageCount: 0,
|
|
||||||
lastSeenMessageId: '0',
|
|
||||||
chatId: chatId,
|
|
||||||
minId: undefined,
|
|
||||||
maxId: undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clear = (storage) => {
|
|
||||||
const failedMessageIds = []
|
|
||||||
|
|
||||||
for (const message of storage.messages) {
|
|
||||||
if (message.error) {
|
|
||||||
failedMessageIds.push(message.id)
|
|
||||||
} else {
|
|
||||||
delete storage.idIndex[message.id]
|
|
||||||
delete storage.idempotencyKeyIndex[message.idempotency_key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
storage.messages = storage.messages.filter(m => failedMessageIds.includes(m.id))
|
|
||||||
storage.newMessageCount = 0
|
|
||||||
storage.lastSeenMessageId = '0'
|
|
||||||
storage.minId = undefined
|
|
||||||
storage.maxId = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteMessage = (storage, messageId) => {
|
|
||||||
if (!storage) { return }
|
|
||||||
storage.messages = storage.messages.filter(m => m.id !== messageId)
|
|
||||||
delete storage.idIndex[messageId]
|
|
||||||
|
|
||||||
if (storage.maxId === messageId) {
|
|
||||||
const lastMessage = _.maxBy(storage.messages, 'id')
|
|
||||||
storage.maxId = lastMessage.id
|
|
||||||
}
|
|
||||||
|
|
||||||
if (storage.minId === messageId) {
|
|
||||||
const firstMessage = _.minBy(storage.messages, 'id')
|
|
||||||
storage.minId = firstMessage.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cullOlderMessages = (storage) => {
|
|
||||||
const maxIndex = storage.messages.length
|
|
||||||
const minIndex = maxIndex - 50
|
|
||||||
if (maxIndex <= 50) return
|
|
||||||
|
|
||||||
storage.messages = _.sortBy(storage.messages, ['id'])
|
|
||||||
storage.minId = storage.messages[minIndex].id
|
|
||||||
for (const message of storage.messages) {
|
|
||||||
if (message.id < storage.minId) {
|
|
||||||
delete storage.idIndex[message.id]
|
|
||||||
delete storage.idempotencyKeyIndex[message.idempotency_key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
storage.messages = storage.messages.slice(minIndex, maxIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMessageError = (storage, fakeId, isRetry) => {
|
|
||||||
if (!storage) { return }
|
|
||||||
const fakeMessage = storage.idIndex[fakeId]
|
|
||||||
if (fakeMessage) {
|
|
||||||
fakeMessage.error = true
|
|
||||||
fakeMessage.pending = false
|
|
||||||
if (!isRetry) {
|
|
||||||
// Ensure the failed message doesn't stay at the bottom of the list.
|
|
||||||
const lastPersistedMessage = _.orderBy(storage.messages, ['pending', 'id'], ['asc', 'desc'])[0]
|
|
||||||
if (lastPersistedMessage) {
|
|
||||||
const oldId = fakeMessage.id
|
|
||||||
fakeMessage.id = `${lastPersistedMessage.id}-${new Date().getTime()}`
|
|
||||||
storage.idIndex[fakeMessage.id] = fakeMessage
|
|
||||||
delete storage.idIndex[oldId]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const add = (storage, { messages: newMessages, updateMaxId = true }) => {
|
|
||||||
if (!storage) { return }
|
|
||||||
for (let i = 0; i < newMessages.length; i++) {
|
|
||||||
const message = newMessages[i]
|
|
||||||
|
|
||||||
// sanity check
|
|
||||||
if (message.chat_id !== storage.chatId) { return }
|
|
||||||
|
|
||||||
if (message.fakeId) {
|
|
||||||
const fakeMessage = storage.idIndex[message.fakeId]
|
|
||||||
if (fakeMessage) {
|
|
||||||
// In case the same id exists (chat update before POST response)
|
|
||||||
// make sure to remove the older duplicate message.
|
|
||||||
if (storage.idIndex[message.id]) {
|
|
||||||
delete storage.idIndex[message.id]
|
|
||||||
storage.messages = storage.messages.filter(msg => msg.id !== message.id)
|
|
||||||
}
|
|
||||||
Object.assign(fakeMessage, message, { error: false })
|
|
||||||
delete fakeMessage['fakeId']
|
|
||||||
storage.idIndex[fakeMessage.id] = fakeMessage
|
|
||||||
delete storage.idIndex[message.fakeId]
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!storage.minId || (!message.pending && message.id < storage.minId)) {
|
|
||||||
storage.minId = message.id
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!storage.maxId || message.id > storage.maxId) {
|
|
||||||
if (updateMaxId) {
|
|
||||||
storage.maxId = message.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!storage.idIndex[message.id] && !isConfirmation(storage, message)) {
|
|
||||||
if (storage.lastSeenMessageId < message.id) {
|
|
||||||
storage.newMessageCount++
|
|
||||||
}
|
|
||||||
storage.idIndex[message.id] = message
|
|
||||||
storage.messages.push(storage.idIndex[message.id])
|
|
||||||
storage.idempotencyKeyIndex[message.idempotency_key] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isConfirmation = (storage, message) => {
|
|
||||||
if (!message.idempotency_key) return
|
|
||||||
return storage.idempotencyKeyIndex[message.idempotency_key]
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetNewMessageCount = (storage) => {
|
|
||||||
if (!storage) { return }
|
|
||||||
storage.newMessageCount = 0
|
|
||||||
storage.lastSeenMessageId = storage.maxId
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 = _.orderBy(storage.messages, ['pending', 'id'], ['asc', 'asc'])
|
|
||||||
const firstMessage = messages[0]
|
|
||||||
let previousMessage = messages[messages.length - 1]
|
|
||||||
let currentMessageChainId
|
|
||||||
|
|
||||||
if (firstMessage) {
|
|
||||||
const date = new Date(firstMessage.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 (previousMessage && previousMessage.date < date) {
|
|
||||||
result.push({
|
|
||||||
type: 'date',
|
|
||||||
date,
|
|
||||||
id: date.getTime().toString()
|
|
||||||
})
|
|
||||||
|
|
||||||
previousMessage['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 ((previousMessage && previousMessage.data && previousMessage.data.account_id) !== message.account_id || afterDate) {
|
|
||||||
currentMessageChainId = _.uniqueId()
|
|
||||||
object['isHead'] = true
|
|
||||||
object['messageChainId'] = currentMessageChainId
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(object)
|
|
||||||
previousMessage = object
|
|
||||||
afterDate = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChatService = {
|
|
||||||
add,
|
|
||||||
empty,
|
|
||||||
getView,
|
|
||||||
deleteMessage,
|
|
||||||
cullOlderMessages,
|
|
||||||
resetNewMessageCount,
|
|
||||||
clear,
|
|
||||||
handleMessageError
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ChatService
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js'
|
|
||||||
|
|
||||||
export const maybeShowChatNotification = (store, chat) => {
|
|
||||||
if (!chat.lastMessage) return
|
|
||||||
if (store.rootState.chats.currentChatId === chat.id && !document.hidden) return
|
|
||||||
if (store.rootState.users.currentUser.id === chat.lastMessage.account_id) return
|
|
||||||
|
|
||||||
const opts = {
|
|
||||||
tag: chat.lastMessage.id,
|
|
||||||
title: chat.account.name,
|
|
||||||
icon: chat.account.profile_image_url,
|
|
||||||
body: chat.lastMessage.content
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chat.lastMessage.attachment && chat.lastMessage.attachment.type === 'image') {
|
|
||||||
opts.image = chat.lastMessage.attachment.preview_url
|
|
||||||
}
|
|
||||||
|
|
||||||
showDesktopNotification(store.rootState, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const buildFakeMessage = ({ content, chatId, attachments, userId, idempotencyKey }) => {
|
|
||||||
const fakeMessage = {
|
|
||||||
content,
|
|
||||||
chat_id: chatId,
|
|
||||||
created_at: new Date(),
|
|
||||||
id: `${new Date().getTime()}`,
|
|
||||||
attachments: attachments,
|
|
||||||
account_id: userId,
|
|
||||||
idempotency_key: idempotencyKey,
|
|
||||||
emojis: [],
|
|
||||||
pending: true,
|
|
||||||
isNormalized: true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attachments[0]) {
|
|
||||||
fakeMessage.attachment = attachments[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
return fakeMessage
|
|
||||||
}
|
|
|
@ -94,7 +94,6 @@ export const parseUser = (data) => {
|
||||||
|
|
||||||
output.background_image = data.pleroma.background_image
|
output.background_image = data.pleroma.background_image
|
||||||
output.favicon = data.pleroma.favicon
|
output.favicon = data.pleroma.favicon
|
||||||
output.token = data.pleroma.chat_token
|
|
||||||
|
|
||||||
if (relationship) {
|
if (relationship) {
|
||||||
output.relationship = relationship
|
output.relationship = relationship
|
||||||
|
@ -200,7 +199,6 @@ export const parseUser = (data) => {
|
||||||
: data.pleroma.deactivated // old backend
|
: data.pleroma.deactivated // old backend
|
||||||
|
|
||||||
output.notification_settings = data.pleroma.notification_settings
|
output.notification_settings = data.pleroma.notification_settings
|
||||||
output.unread_chat_count = data.pleroma.unread_chat_count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
output.tags = output.tags || []
|
output.tags = output.tags || []
|
||||||
|
@ -403,7 +401,7 @@ export const parseNotification = (data) => {
|
||||||
? parseStatus(data.notice.favorited_status)
|
? parseStatus(data.notice.favorited_status)
|
||||||
: parsedNotice
|
: parsedNotice
|
||||||
output.action = parsedNotice
|
output.action = parsedNotice
|
||||||
output.from_profile = output.type === 'pleroma:chat_mention' ? parseUser(data.account) : parseUser(data.from_profile)
|
output.from_profile = parseUser(data.from_profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
output.created_at = new Date(data.created_at)
|
output.created_at = new Date(data.created_at)
|
||||||
|
@ -429,34 +427,3 @@ export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
|
||||||
minId: flakeId ? minId : parseInt(minId, 10)
|
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
|
|
||||||
output.emojis = message.emojis
|
|
||||||
output.content = message.content
|
|
||||||
if (message.attachment) {
|
|
||||||
output.attachments = [parseAttachment(message.attachment)]
|
|
||||||
} else {
|
|
||||||
output.attachments = []
|
|
||||||
}
|
|
||||||
output.pending = !!message.pending
|
|
||||||
output.error = false
|
|
||||||
output.idempotency_key = message.idempotency_key
|
|
||||||
output.isNormalized = true
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
|
@ -106,8 +106,7 @@ export const generateRadii = (input) => {
|
||||||
avatar: 5,
|
avatar: 5,
|
||||||
avatarAlt: 50,
|
avatarAlt: 50,
|
||||||
tooltip: 2,
|
tooltip: 2,
|
||||||
attachment: 5,
|
attachment: 5
|
||||||
chatMessage: inputRadii.panel
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -23,9 +23,7 @@ export const LAYERS = {
|
||||||
inputTopBar: 'topBar',
|
inputTopBar: 'topBar',
|
||||||
alert: 'bg',
|
alert: 'bg',
|
||||||
alertPanel: 'panel',
|
alertPanel: 'panel',
|
||||||
poll: 'bg',
|
poll: 'bg'
|
||||||
chatBg: 'underlay',
|
|
||||||
chatMessage: 'chatBg'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* By default opacity slots have 1 as default opacity
|
/* By default opacity slots have 1 as default opacity
|
||||||
|
@ -707,58 +705,5 @@ export const SLOT_INHERITANCE = {
|
||||||
layer: 'badge',
|
layer: 'badge',
|
||||||
variant: 'badgeNotification',
|
variant: 'badgeNotification',
|
||||||
textColor: 'bw'
|
textColor: 'bw'
|
||||||
},
|
|
||||||
|
|
||||||
chatBg: {
|
|
||||||
depends: ['bg']
|
|
||||||
},
|
|
||||||
|
|
||||||
chatMessageIncomingBg: {
|
|
||||||
depends: ['chatBg']
|
|
||||||
},
|
|
||||||
|
|
||||||
chatMessageIncomingText: {
|
|
||||||
depends: ['text'],
|
|
||||||
layer: 'chatMessage',
|
|
||||||
variant: 'chatMessageIncomingBg',
|
|
||||||
textColor: true
|
|
||||||
},
|
|
||||||
|
|
||||||
chatMessageIncomingLink: {
|
|
||||||
depends: ['link'],
|
|
||||||
layer: 'chatMessage',
|
|
||||||
variant: 'chatMessageIncomingBg',
|
|
||||||
textColor: 'preserve'
|
|
||||||
},
|
|
||||||
|
|
||||||
chatMessageIncomingBorder: {
|
|
||||||
depends: ['border'],
|
|
||||||
opacity: 'border',
|
|
||||||
color: (mod, border) => brightness(2 * mod, border).rgb
|
|
||||||
},
|
|
||||||
|
|
||||||
chatMessageOutgoingBg: {
|
|
||||||
depends: ['chatMessageIncomingBg'],
|
|
||||||
color: (mod, chatMessage) => brightness(5 * mod, chatMessage).rgb
|
|
||||||
},
|
|
||||||
|
|
||||||
chatMessageOutgoingText: {
|
|
||||||
depends: ['text'],
|
|
||||||
layer: 'chatMessage',
|
|
||||||
variant: 'chatMessageOutgoingBg',
|
|
||||||
textColor: true
|
|
||||||
},
|
|
||||||
|
|
||||||
chatMessageOutgoingLink: {
|
|
||||||
depends: ['link'],
|
|
||||||
layer: 'chatMessage',
|
|
||||||
variant: 'chatMessageOutgoingBg',
|
|
||||||
textColor: 'preserve'
|
|
||||||
},
|
|
||||||
|
|
||||||
chatMessageOutgoingBorder: {
|
|
||||||
depends: ['chatMessageOutgoingBg'],
|
|
||||||
opacity: 'border',
|
|
||||||
color: (mod, border) => brightness(2 * mod, border).rgb
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,108 +0,0 @@
|
||||||
import chatService from '../../../../../src/services/chat_service/chat_service.js'
|
|
||||||
|
|
||||||
const message1 = {
|
|
||||||
id: '9wLkdcmQXD21Oy8lEX',
|
|
||||||
idempotency_key: '1',
|
|
||||||
created_at: (new Date('2020-06-22T18:45:53.000Z'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const message2 = {
|
|
||||||
id: '9wLkdp6ihaOVdNj8Wu',
|
|
||||||
idempotency_key: '2',
|
|
||||||
account_id: '9vmRb29zLQReckr5ay',
|
|
||||||
created_at: (new Date('2020-06-22T18:45:56.000Z'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const message3 = {
|
|
||||||
id: '9wLke9zL4Dy4OZR2RM',
|
|
||||||
idempotency_key: '3',
|
|
||||||
account_id: '9vmRb29zLQReckr5ay',
|
|
||||||
created_at: (new Date('2020-07-22T18:45:59.000Z'))
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('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.maxId).to.eql(message1.id)
|
|
||||||
expect(chat.minId).to.eql(message1.id)
|
|
||||||
expect(chat.newMessageCount).to.eql(1)
|
|
||||||
|
|
||||||
chatService.add(chat, { messages: [ message2 ] })
|
|
||||||
expect(chat.maxId).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)
|
|
||||||
expect(chat.lastSeenMessageId).to.eql(message2.id)
|
|
||||||
|
|
||||||
// Add message with higher id
|
|
||||||
chatService.add(chat, { messages: [ message3 ] })
|
|
||||||
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.maxId).to.eql(message3.id)
|
|
||||||
expect(chat.minId).to.eql(message1.id)
|
|
||||||
|
|
||||||
chatService.deleteMessage(chat, message3.id)
|
|
||||||
expect(chat.maxId).to.eql(message2.id)
|
|
||||||
expect(chat.minId).to.eql(message1.id)
|
|
||||||
|
|
||||||
chatService.deleteMessage(chat, message1.id)
|
|
||||||
expect(chat.maxId).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'])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('.cullOlderMessages', () => {
|
|
||||||
it('keeps 50 newest messages and idIndex matches', () => {
|
|
||||||
const chat = chatService.empty()
|
|
||||||
|
|
||||||
for (let i = 100; i > 0; i--) {
|
|
||||||
// Use decimal values with toFixed to hack together constant length predictable strings
|
|
||||||
chatService.add(chat, { messages: [{ ...message1, id: 'a' + (i / 1000).toFixed(3), idempotency_key: i }] })
|
|
||||||
}
|
|
||||||
chatService.cullOlderMessages(chat)
|
|
||||||
expect(chat.messages.length).to.eql(50)
|
|
||||||
expect(chat.messages[0].id).to.eql('a0.051')
|
|
||||||
expect(chat.minId).to.eql('a0.051')
|
|
||||||
expect(chat.messages[49].id).to.eql('a0.100')
|
|
||||||
expect(Object.keys(chat.idIndex).length).to.eql(50)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
Loading…
Reference in a new issue