commit
da94935aaa
@ -0,0 +1,8 @@
|
|||||||
|
# Introduction to Pleroma-FE
|
||||||
|
## What is Pleroma-FE?
|
||||||
|
|
||||||
|
Pleroma-FE is the default user-facing frontend for Pleroma. It's user interface is modeled after Qvitter which is modeled after an older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options.
|
||||||
|
|
||||||
|
## How can I use it?
|
||||||
|
|
||||||
|
If your instance uses Pleroma-FE, you can acces it by going to your instance (e.g. <https://pleroma.soykaf.com>). You can read more about it's basic functionality in the [Pleroma-FE User Guide](./USER_GUIDE.md). We also have [a guide for administrators](./CONFIGURATION.md) and for [hackers/contributors](./HACKING.md).
|
@ -0,0 +1,17 @@
|
|||||||
|
import Timeline from '../timeline/timeline.vue'
|
||||||
|
|
||||||
|
const Bookmarks = {
|
||||||
|
computed: {
|
||||||
|
timeline () {
|
||||||
|
return this.$store.state.statuses.timelines.bookmarks
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Timeline
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Bookmarks
|
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<Timeline
|
||||||
|
:title="$t('nav.bookmarks')"
|
||||||
|
:timeline="timeline"
|
||||||
|
:timeline-name="'bookmarks'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./bookmark_timeline.js"></script>
|
@ -0,0 +1,333 @@
|
|||||||
|
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 { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight } from './chat_layout_utils.js'
|
||||||
|
|
||||||
|
const BOTTOMED_OUT_OFFSET = 10
|
||||||
|
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150
|
||||||
|
const SAFE_RESIZE_TIME_OFFSET = 100
|
||||||
|
|
||||||
|
const Chat = {
|
||||||
|
components: {
|
||||||
|
ChatMessage,
|
||||||
|
ChatTitle,
|
||||||
|
PostStatusForm
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
jumpToBottomButtonVisible: false,
|
||||||
|
hoveredMessageChainId: undefined,
|
||||||
|
lastScrollPosition: {},
|
||||||
|
scrollableContainerHeight: '100%',
|
||||||
|
errorLoadingChat: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.startFetching()
|
||||||
|
window.addEventListener('resize', this.handleLayoutChange)
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
window.addEventListener('scroll', this.handleScroll)
|
||||||
|
if (typeof document.hidden !== 'undefined') {
|
||||||
|
document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateScrollableContainerHeight()
|
||||||
|
this.handleResize()
|
||||||
|
})
|
||||||
|
this.setChatLayout()
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
window.removeEventListener('scroll', this.handleScroll)
|
||||||
|
window.removeEventListener('resize', this.handleLayoutChange)
|
||||||
|
this.unsetChatLayout()
|
||||||
|
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 })
|
||||||
|
} 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.mobileLayout,
|
||||||
|
layoutHeight: state => state.interface.layoutHeight,
|
||||||
|
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({ forceRead: !document.hidden })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
'$route': function () {
|
||||||
|
this.startFetching()
|
||||||
|
},
|
||||||
|
layoutHeight () {
|
||||||
|
this.handleResize({ expand: true })
|
||||||
|
},
|
||||||
|
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()
|
||||||
|
this.updateScrollableContainerHeight()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleVisibilityChange () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (!document.hidden && this.bottomedOut(BOTTOMED_OUT_OFFSET)) {
|
||||||
|
this.scrollDown({ forceRead: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setChatLayout () {
|
||||||
|
// This is a hacky way to adjust the global layout to the mobile chat (without modifying the rest of the app).
|
||||||
|
// This layout prevents empty spaces from being visible at the bottom
|
||||||
|
// of the chat on iOS Safari (`safe-area-inset`) when
|
||||||
|
// - the on-screen keyboard appears and the user starts typing
|
||||||
|
// - the user selects the text inside the input area
|
||||||
|
// - the user selects and deletes the text that is multiple lines long
|
||||||
|
// TODO: unify the chat layout with the global layout.
|
||||||
|
let html = document.querySelector('html')
|
||||||
|
if (html) {
|
||||||
|
html.classList.add('chat-layout')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateScrollableContainerHeight()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
unsetChatLayout () {
|
||||||
|
let html = document.querySelector('html')
|
||||||
|
if (html) {
|
||||||
|
html.classList.remove('chat-layout')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleLayoutChange () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateScrollableContainerHeight()
|
||||||
|
this.scrollDown()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// Ensures the proper position of the posting form in the mobile layout (the mobile browser panel does not overlap or hide it)
|
||||||
|
updateScrollableContainerHeight () {
|
||||||
|
const header = this.$refs.header
|
||||||
|
const footer = this.$refs.footer
|
||||||
|
const inner = this.mobileLayout ? window.document.body : this.$refs.inner
|
||||||
|
this.scrollableContainerHeight = scrollableContainerHeight(inner, header, footer) + 'px'
|
||||||
|
},
|
||||||
|
// Preserves the scroll position when OSK appears or the posting form changes its height.
|
||||||
|
handleResize (opts = {}) {
|
||||||
|
const { expand = false, delayed = false } = opts
|
||||||
|
|
||||||
|
if (delayed) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.handleResize({ ...opts, delayed: false })
|
||||||
|
}, SAFE_RESIZE_TIME_OFFSET)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateScrollableContainerHeight()
|
||||||
|
|
||||||
|
const { offsetHeight = undefined } = this.lastScrollPosition
|
||||||
|
this.lastScrollPosition = getScrollPosition(this.$refs.scrollable)
|
||||||
|
|
||||||
|
const diff = this.lastScrollPosition.offsetHeight - offsetHeight
|
||||||
|
if (diff < 0 || (!this.bottomedOut() && expand)) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateScrollableContainerHeight()
|
||||||
|
this.$refs.scrollable.scrollTo({
|
||||||
|
top: this.$refs.scrollable.scrollTop - diff,
|
||||||
|
left: 0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
scrollDown (options = {}) {
|
||||||
|
const { behavior = 'auto', forceRead = false } = options
|
||||||
|
const scrollable = this.$refs.scrollable
|
||||||
|
if (!scrollable) { return }
|
||||||
|
this.$nextTick(() => {
|
||||||
|
scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior })
|
||||||
|
})
|
||||||
|
if (forceRead || this.newMessageCount > 0) {
|
||||||
|
this.readChat()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
readChat () {
|
||||||
|
if (!(this.currentChatMessageService && this.currentChatMessageService.lastMessage)) { return }
|
||||||
|
if (document.hidden) { return }
|
||||||
|
const lastReadId = this.currentChatMessageService.lastMessage.id
|
||||||
|
this.$store.dispatch('readChat', { id: this.currentChat.id, lastReadId })
|
||||||
|
},
|
||||||
|
bottomedOut (offset) {
|
||||||
|
return isBottomedOut(this.$refs.scrollable, offset)
|
||||||
|
},
|
||||||
|
reachedTop () {
|
||||||
|
const scrollable = this.$refs.scrollable
|
||||||
|
return scrollable && scrollable.scrollTop <= 0
|
||||||
|
},
|
||||||
|
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
|
||||||
|
if (this.newMessageCount > 0) {
|
||||||
|
this.readChat()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.jumpToBottomButtonVisible = true
|
||||||
|
}
|
||||||
|
}, 100),
|
||||||
|
handleScrollUp (positionBeforeLoading) {
|
||||||
|
const positionAfterLoading = getScrollPosition(this.$refs.scrollable)
|
||||||
|
this.$refs.scrollable.scrollTo({
|
||||||
|
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
|
||||||
|
left: 0
|
||||||
|
})
|
||||||
|
},
|
||||||
|
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.lastMessage && chatMessageService.lastMessage.id
|
||||||
|
|
||||||
|
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.$refs.scrollable)
|
||||||
|
this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (fetchOlderMessages) {
|
||||||
|
this.handleScrollUp(positionBeforeUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFirstFetch) {
|
||||||
|
this.updateScrollableContainerHeight()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
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: () => setInterval(() => this.fetchChat({ fetchLatest: true }), 5000)
|
||||||
|
})
|
||||||
|
this.fetchChat({ isFirstFetch: true })
|
||||||
|
},
|
||||||
|
sendMessage ({ status, media }) {
|
||||||
|
const params = {
|
||||||
|
id: this.currentChat.id,
|
||||||
|
content: status
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media[0]) {
|
||||||
|
params.mediaId = media[0].id
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.backendInteractor.sendChatMessage(params)
|
||||||
|
.then(data => {
|
||||||
|
this.$store.dispatch('addChatMessages', { chatId: this.currentChat.id, messages: [data] }).then(() => {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.handleResize()
|
||||||
|
// When the posting form size changes because of a media attachment, we need an extra resize
|
||||||
|
// to account for the potential delay in the DOM update.
|
||||||
|
setTimeout(() => {
|
||||||
|
this.updateScrollableContainerHeight()
|
||||||
|
}, SAFE_RESIZE_TIME_OFFSET)
|
||||||
|
this.scrollDown({ forceRead: true })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error sending message', error)
|
||||||
|
return {
|
||||||
|
error: this.$t('chats.error_sending_message')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goBack () {
|
||||||
|
this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Chat
|
@ -0,0 +1,162 @@
|
|||||||
|
.chat-view {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.chat-title {
|
||||||
|
// prevents chat header jumping on when the user avatar loads
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-view-inner {
|
||||||
|
height: auto;
|
||||||
|
width: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
display: flex;
|
||||||
|
margin: 0.5em 0.5em 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-view-body {
|
||||||
|
background-color: var(--chatBg, $fallback--bg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
min-height: 100%;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable-message-list {
|
||||||
|
padding: 0 0.8em;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-view-heading {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
top: 50px;
|
||||||
|
display: flex;
|
||||||
|
z-index: 2;
|
||||||
|
position: sticky;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.go-back-button {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 1.4em;
|
||||||
|
|
||||||
|
i {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 0px 1px 1px rgba(0, 0, 0, 0.3), 0px 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 10;
|
||||||
|
transition: 0.35s all;
|
||||||
|
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 1em;
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--text, $fallback--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unread-message-count {
|
||||||
|
font-size: 0.8em;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
border-radius: 100%;
|
||||||
|
margin-top: -1rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-loading-error {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.error {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 800px) {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.chat-view-inner {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-view-body {
|
||||||
|
display: flex;
|
||||||
|
min-height: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-view-heading {
|
||||||
|
position: static;
|
||||||
|
z-index: 9999;
|
||||||
|
top: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable-message-list {
|
||||||
|
display: unset;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: sticky;
|
||||||
|
bottom: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chat-view">
|
||||||
|
<div class="chat-view-inner">
|
||||||
|
<div
|
||||||
|
id="nav"
|
||||||
|
ref="inner"
|
||||||
|
class="panel-default panel chat-view-body"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="header"
|
||||||
|
class="panel-heading chat-view-heading mobile-hidden"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="go-back-button"
|
||||||
|
@click="goBack"
|
||||||
|
>
|
||||||
|
<i class="button-icon icon-left-open" />
|
||||||
|
</a>
|
||||||
|
<div class="title text-center">
|
||||||
|
<ChatTitle
|
||||||
|
:user="recipient"
|
||||||
|
:with-avatar="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="scrollable"
|
||||||
|
class="scrollable-message-list"
|
||||||
|
:style="{ height: scrollableContainerHeight }"
|
||||||
|
@scroll="handleScroll"
|
||||||
|
>
|
||||||
|
<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' })"
|
||||||
|
>
|
||||||
|
<i class="icon-down-open">
|
||||||
|
<div
|
||||||
|
v-if="newMessageCount"
|
||||||
|
class="badge badge-notification unread-chat-count unread-message-count"
|
||||||
|
>
|
||||||
|
{{ newMessageCount }}
|
||||||
|
</div>
|
||||||
|
</i>
|
||||||
|
</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"
|
||||||
|
: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>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./chat.js"></script>
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
@import './chat.scss';
|
||||||
|
</style>
|
@ -0,0 +1,26 @@
|
|||||||
|
// Captures a scroll position
|
||||||
|
export const getScrollPosition = (el) => {
|
||||||
|
return {
|
||||||
|
scrollTop: el.scrollTop,
|
||||||
|
scrollHeight: el.scrollHeight,
|
||||||
|
offsetHeight: el.offsetHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A helper function that is used to keep the scroll position fixed as the new elements are added to the top
|
||||||
|
// Takes two scroll positions, before and after the update.
|
||||||
|
export const getNewTopPosition = (previousPosition, newPosition) => {
|
||||||
|
return previousPosition.scrollTop + (newPosition.scrollHeight - previousPosition.scrollHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isBottomedOut = (el, offset = 0) => {
|
||||||
|
if (!el) { return }
|
||||||
|
const scrollHeight = el.scrollTop + offset
|
||||||
|
const totalHeight = el.scrollHeight - el.offsetHeight
|
||||||
|
return totalHeight <= scrollHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height of the scrollable container. The dynamic height is needed to ensure the mobile browser panel doesn't overlap or hide the posting form.
|
||||||
|
export const scrollableContainerHeight = (inner, header, footer) => {
|
||||||
|
return inner.offsetHeight - header.clientHeight - footer.clientHeight
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import { mapState, mapGetters } from 'vuex'
|
||||||
|
import ChatListItem from '../chat_list_item/chat_list_item.vue'
|
||||||
|
import ChatNew from '../chat_new/chat_new.vue'
|
||||||
|
import List from '../list/list.vue'
|
||||||
|
|
||||||
|
const ChatList = {
|
||||||
|
components: {
|
||||||
|
ChatListItem,
|
||||||
|
List,
|
||||||
|
ChatNew
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
currentUser: state => state.users.currentUser
|
||||||
|
}),
|
||||||
|
...mapGetters(['sortedChatList'])
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isNew: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.$store.dispatch('fetchChats', { latest: true })
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
cancelNewChat () {
|
||||||
|
this.isNew = false
|
||||||
|
this.$store.dispatch('fetchChats', { latest: true })
|
||||||
|
},
|
||||||
|
newChat () {
|
||||||
|
this.isNew = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatList
|
@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="isNew">
|
||||||
|
<ChatNew @cancel="cancelNewChat" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="chat-list panel panel-default"
|
||||||
|
>
|
||||||
|
<div class="panel-heading">
|
||||||
|
<span class="title">
|
||||||
|
{{ $t("chats.chats") }}
|
||||||
|
</span>
|
||||||
|
<button @click="newChat">
|
||||||
|
{{ $t("chats.new") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div
|
||||||
|
v-if="sortedChatList.length > 0"
|
||||||
|
class="timeline"
|
||||||
|
>
|
||||||
|
<List :items="sortedChatList">
|
||||||
|
<template
|
||||||
|
slot="item"
|
||||||
|
slot-scope="{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>
|
@ -0,0 +1,67 @@
|
|||||||
|
import { mapState } from 'vuex'
|
||||||
|
import StatusContent 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,
|
||||||
|
StatusContent
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
currentUser: state => state.users.currentUser
|
||||||
|
}),
|
||||||
|
attachmentInfo () {
|
||||||
|
if (this.chat.lastMessage.attachments.length === 0) { return }
|
||||||
|
|
||||||
|
const types = this.chat.lastMessage.attachments.map(file => fileType.fileType(file.mimetype))
|
||||||
|
if (types.includes('video')) {
|
||||||
|
return this.$t('file_type.video')
|
||||||
|
} else if (types.includes('audio')) {
|
||||||
|
return this.$t('file_type.audio')
|
||||||
|
} else if (types.includes('image')) {
|
||||||
|
return this.$t('file_type.image')
|
||||||
|
} else {
|
||||||
|
return this.$t('file_type.file')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
messageForStatusContent () {
|
||||||
|
const message = this.chat.lastMessage
|
||||||
|
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: '',
|
||||||
|
statusnet_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
|
@ -0,0 +1,94 @@
|
|||||||
|
.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: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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.still-image {
|
||||||
|
border-radius: $fallback--avatarAltRadius;
|
||||||
|
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-body {
|
||||||
|
img.emoji {
|
||||||
|
width: 1.4em;
|
||||||
|
height: 1.4em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-wrapper {
|
||||||
|
line-height: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-line {
|
||||||
|
padding-right: 1em;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
<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>
|
||||||
|
<div class="chat-preview">
|
||||||
|
<StatusContent
|
||||||
|
:status="messageForStatusContent"
|
||||||
|
:single-line="true"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="chat.unread > 0"
|
||||||
|
class="badge badge-notification unread-chat-count"
|
||||||
|
>
|
||||||
|
{{ chat.unread }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="time-wrapper">
|
||||||
|
<Timeago
|
||||||
|
:time="chat.updated_at"
|
||||||
|
:auto-update="60"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./chat_list_item.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
@import './chat_list_item.scss';
|
||||||
|
</style>
|
@ -0,0 +1,96 @@
|
|||||||
|
import { mapState, mapGetters } from 'vuex'
|
||||||
|
import Popover from '../popover/popover.vue'
|
||||||
|
import Attachment from '../attachment/attachment.vue'
|
||||||
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
|
import Gallery from '../gallery/gallery.vue'
|
||||||
|
import LinkPreview from '../link-preview/link-preview.vue'
|
||||||
|
import StatusContent from '../status_content/status_content.vue'
|
||||||
|
import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
|
||||||
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
|
||||||
|
const ChatMessage = {
|
||||||
|
name: 'ChatMessage',
|
||||||
|
props: [
|
||||||
|
'author',
|
||||||
|
'edited',
|
||||||
|
'noHeading',
|
||||||
|
'chatViewItem',
|
||||||
|
'hoveredMessageChain'
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
Popover,
|
||||||
|
Attachment,
|
||||||
|
StatusContent,
|
||||||
|
UserAvatar,
|
||||||
|
Gallery,
|
||||||
|
LinkPreview,
|
||||||
|
ChatMessageDate
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
// Returns HH:MM (hours and minutes) in local time.
|
||||||
|
createdAt () {
|
||||||
|
const time = this.chatViewItem.data.created_at
|
||||||
|
return time.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hour12: false })
|
||||||
|
},
|
||||||
|
isCurrentUser () {
|
||||||
|
return this.message.account_id === this.currentUser.id
|
||||||
|
},
|
||||||
|
message () {
|
||||||
|
return this.chatViewItem.data
|
||||||
|
},
|
||||||
|
userProfileLink () {
|
||||||
|
return generateProfileLink(this.author.id, this.author.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||||
|
},
|
||||||
|
isMessage () {
|
||||||
|
return this.chatViewItem.type === 'message'
|
||||||
|
},
|
||||||
|
messageForStatusContent () {
|
||||||
|
return {
|
||||||
|
summary: '',
|
||||||
|
statusnet_html: this.message.content,
|
||||||
|
text: this.message.content,
|
||||||
|
attachments: this.message.attachments
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hasAttachment () {
|
||||||
|
return this.message.attachments.length > 0
|
||||||
|
},
|
||||||
|
...mapState({
|
||||||
|
betterShadow: state => state.interface.browserSupport.cssFilter,
|
||||||
|
currentUser: state => state.users.currentUser,
|
||||||
|
restrictedNicknames: state => state.instance.restrictedNicknames
|
||||||
|
}),
|
||||||
|
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
|
@ -0,0 +1,164 @@
|
|||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-ellipsis {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover, .extra-button-popover.open & {
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--text, $fallback--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
border-radius: $fallback--chatMessageRadius;
|
||||||
|
border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover {
|
||||||
|
width: 12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message {
|
||||||
|
display: flex;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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%;
|
||||||
|
|
||||||
|
.gallery-row {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
border-radius: $fallback--chatMessageRadius;
|
||||||
|
border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
|
||||||
|
display: flex;
|
||||||
|
padding: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.created-at {
|
||||||
|
position: relative;
|
||||||
|
float: right;
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin: -1em 0 -0.5em 0;
|
||||||
|
font-style: italic;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.without-attachment {
|
||||||
|
.status-content {
|
||||||
|
&::after {
|
||||||
|
margin-right: 5.4em;
|
||||||
|
content: " ";
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.incoming {
|
||||||
|
a {
|
||||||
|
color: var(--chatMessageIncomingLink, $fallback--link);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
color: var(--chatMessageIncomingText, $fallback--text);
|
||||||
|
background-color: var(--chatMessageIncomingBg, $fallback--bg);
|
||||||
|
border: 1px solid var(--chatMessageIncomingBorder, --border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.created-at {
|
||||||
|
a {
|
||||||
|
color: var(--chatMessageIncomingText, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
<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 }"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<div slot="content">
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<button
|
||||||
|
class="dropdown-item dropdown-item-icon"
|
||||||
|
@click="deleteMessage"
|
||||||
|
>
|
||||||
|
<i class="icon-cancel" /> {{ $t("chats.delete") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
slot="trigger"
|
||||||
|
:title="$t('chats.more')"
|
||||||
|
>
|
||||||
|
<i class="icon-ellipsis" />
|
||||||
|
</button>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<StatusContent
|
||||||
|
:status="messageForStatusContent"
|
||||||
|
:full-content="true"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
slot="footer"
|
||||||
|
class="created-at"
|
||||||
|
>
|
||||||
|
{{ createdAt }}
|
||||||
|
</span>
|
||||||
|
</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>
|
@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<time>
|
||||||
|
{{ displayDate }}
|
||||||
|
</time>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Timeago',
|
||||||
|
props: ['date'],
|
||||||
|
computed: {
|
||||||
|
displayDate () {
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
if (this.date.getTime() === today.getTime()) {
|
||||||
|
return this.$t('display_date.today')
|
||||||
|
} else {
|
||||||
|
return this.date.toLocaleDateString('en', { day: 'numeric', month: 'long' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -0,0 +1,73 @@
|
|||||||
|
import { mapState, mapGetters } from 'vuex'
|
||||||
|
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||||
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
|
|
||||||
|
const chatNew = {
|
||||||
|
components: {
|
||||||
|
BasicUserCard,
|
||||||
|
UserAvatar
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
suggestions: [],
|
||||||
|
userIds: [],
|
||||||
|
loading: false,
|
||||||
|
query: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created () {
|
||||||
|
const { chats } = await this.backendInteractor.chats()
|
||||||
|
chats.forEach(chat => this.suggestions.push(chat.account))
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
users () {
|
||||||
|
return this.userIds.map(userId => this.findUser(userId))
|
||||||
|
},
|
||||||
|
availableUsers () {
|
||||||
|
if (this.query.length !== 0) {
|
||||||
|
return this.users
|
||||||
|
} else {
|
||||||
|
return this.suggestions
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...mapState({
|
||||||
|
currentUser: state => state.users.currentUser,
|
||||||
|
backendInteractor: state => state.api.backendInteractor
|
||||||
|
}),
|
||||||
|
...mapGetters(['findUser'])
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goBack () {
|
||||||
|
this.$emit('cancel')
|
||||||
|
},
|
||||||
|
goToChat (user) {
|
||||||
|
this.$router.push({ name: 'chat', params: { recipient_id: user.id } })
|
||||||
|
},
|
||||||
|
onInput () {
|
||||||
|
this.search(this.query)
|
||||||
|
},
|
||||||
|
addUser (user) {
|
||||||
|
this.selectedUserIds.push(user.id)
|
||||||
|
this.query = ''
|
||||||
|
},
|
||||||
|
removeUser (userId) {
|
||||||
|
this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId)
|
||||||
|
},
|
||||||
|
search (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
|
@ -0,0 +1,29 @@
|
|||||||
|
.chat-new {
|
||||||
|
.input-wrap {
|
||||||
|
display: flex;
|
||||||
|
margin: 0.7em 0.5em 0.7em 0.5em;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-search {
|
||||||
|
font-size: 1.5em;
|
||||||
|
float: right;
|
||||||
|
margin-right: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-list {
|
||||||
|
padding-bottom: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basic-user-card:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--selectedPost, $fallback--lightBg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.go-back-button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
id="nav"
|
||||||
|
class="panel-default panel chat-new"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="header"
|
||||||
|
class="panel-heading"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="go-back-button"
|
||||||
|
@click="goBack"
|
||||||
|
>
|
||||||
|
<i class="button-icon icon-left-open" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="input-wrap">
|
||||||
|
<div class="input-search">
|
||||||
|
<i class="button-icon 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>
|
@ -0,0 +1,26 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
|
|
||||||
|
export default Vue.component('chat-title', {
|
||||||
|
name: 'ChatTitle',
|
||||||
|
components: {
|
||||||
|
UserAvatar
|
||||||
|
},
|
||||||
|
props: [
|
||||||
|
'user', 'withAvatar'
|
||||||
|
],
|
||||||
|
computed: {
|
||||||
|
title () {
|
||||||
|
return this.user ? this.user.screen_name : ''
|
||||||
|
},
|
||||||
|
htmlTitle () {
|
||||||
|
return this.user ? this.user.name_html : ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getUserProfileLink (user) {
|
||||||
|
return generateProfileLink(user.id, user.screen_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
|
<div
|
||||||
|
class="chat-title"
|
||||||
|
:title="title"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
v-if="withAvatar && user"
|
||||||
|
:to="getUserProfileLink(user)"
|
||||||
|
>
|
||||||
|
<UserAvatar
|
||||||
|
:user="user"
|
||||||
|
width="23px"
|
||||||
|
height="23px"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
<span
|
||||||
|
class="username"
|
||||||
|
v-html="htmlTitle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- eslint-enable vue/no-v-html -->
|
||||||
|
</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;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.username {
|
||||||
|
max-width: 100%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
.emoji {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
vertical-align: middle;
|
||||||
|
object-fit: contain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.still-image.avatar {
|
||||||
|
width: 23px;
|
||||||
|
height: 23px;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
|
||||||
|
border-radius: $fallback--avatarAltRadius;
|
||||||
|
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||||
|
|
||||||
|
&.animated::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
const GlobalNoticeList = {
|
||||||
|
computed: {
|
||||||
|
notices () {
|
||||||
|
return this.$store.state.interface.globalNotices
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeNotice (notice) {
|
||||||
|
this.$store.dispatch('removeGlobalNotice', notice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GlobalNoticeList
|
@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<div class="global-notice-list">
|
||||||
|
<div
|
||||||
|
v-for="(notice, index) in notices"
|
||||||
|
:key="index"
|
||||||
|
class="alert global-notice"
|
||||||
|
:class="{ ['global-' + notice.level]: true }"
|
||||||
|
>
|
||||||
|
<div class="notice-message">
|
||||||
|
{{ $t(notice.messageKey, notice.messageArgs) }}
|
||||||
|
</div>
|
||||||
|
<i
|
||||||
|
class="button-icon icon-cancel"
|
||||||
|
@click="closeNotice(notice)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./global_notice_list.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.global-notice-list {
|
||||||
|
position: fixed;
|
||||||
|
top: 50px;
|
||||||
|
width: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1001;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.global-notice {
|
||||||
|
pointer-events: auto;
|
||||||
|
text-align: center;
|
||||||
|
width: 40em;
|
||||||
|
max-width: calc(100% - 3em);
|
||||||
|
display: flex;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
line-height: 2em;
|
||||||
|
.notice-message {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
i {
|
||||||
|
flex: 0 0;
|
||||||
|
width: 1.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-error {
|
||||||
|
background-color: var(--alertPopupError, $fallback--cRed);
|
||||||
|
color: var(--alertPopupErrorText, $fallback--text);
|
||||||
|
i {
|
||||||
|
color: var(--alertPopupErrorText, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-warning {
|
||||||
|
background-color: var(--alertPopupWarning, $fallback--cOrange);
|
||||||
|
color: var(--alertPopupWarningText, $fallback--text);
|
||||||
|
i {
|
||||||
|
color: var(--alertPopupWarningText, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-info {
|
||||||
|
background-color: var(--alertPopupNeutral, $fallback--fg);
|
||||||
|
color: var(--alertPopupNeutralText, $fallback--text);
|
||||||
|
i {
|
||||||
|
color: var(--alertPopupNeutralText, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,225 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import { find, omitBy, orderBy, sumBy } from 'lodash'
|
||||||
|
import chatService from '../services/chat_service/chat_service.js'
|
||||||
|
import { parseChat, parseChatMessage } from '../services/entity_normalizer/entity_normalizer.service.js'
|
||||||
|
|
||||||
|
const emptyChatList = () => ({
|
||||||
|
data: [],
|
||||||
|
idStore: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultState = {
|
||||||
|
chatList: emptyChatList(),
|
||||||
|
chatListFetcher: null,
|
||||||
|
openedChats: {},
|
||||||
|
openedChatMessageServices: {},
|
||||||
|
fetcher: undefined,
|
||||||
|
currentChatId: null
|
||||||
|
}
|
||||||
|
|
||||||
|
const getChatById = (state, id) => {
|
||||||
|
return find(state.chatList.data, { id })
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedChatList = (state) => {
|
||||||
|
return orderBy(state.chatList.data, ['updated_at'], ['desc'])
|
||||||
|
}
|
||||||
|
|
||||||
|
const unreadChatCount = (state) => {
|
||||||
|
return sumBy(state.chatList.data, 'unread')
|
||||||
|
}
|
||||||
|
|
||||||
|
const chats = {
|
||||||
|
state: { ...defaultState },
|
||||||
|
getters: {
|
||||||
|
currentChat: state => state.openedChats[state.currentChatId],
|
||||||
|
currentChatMessageService: state => state.openedChatMessageServices[state.currentChatId],
|
||||||
|
findOpenedChatByRecipientId: state => recipientId => find(state.openedChats, c => c.account.id === recipientId),
|
||||||
|
sortedChatList,
|
||||||
|
unreadChatCount
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
// Chat list
|
||||||
|
startFetchingChats ({ dispatch, commit }) {
|
||||||
|
const fetcher = () => {
|
||||||
|
dispatch('fetchChats', { latest: true })
|
||||||
|
}
|
||||||
|
fetcher()
|
||||||
|
commit('setChatListFetcher', {
|
||||||
|
fetcher: () => setInterval(() => { fetcher() }, 5000)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
stopFetchingChats ({ commit }) {
|
||||||
|
commit('setChatListFetcher', { fetcher: undefined })
|
||||||
|
},
|
||||||
|
fetchChats ({ dispatch, rootState, commit }, params = {}) {
|
||||||
|
return rootState.api.backendInteractor.chats()
|
||||||
|
.then(({ chats }) => {
|
||||||
|
dispatch('addNewChats', { chats })
|
||||||
|
return chats
|
||||||
|
})
|
||||||
|
},
|
||||||
|
addNewChats ({ rootState, commit, dispatch, rootGetters }, { chats }) {
|
||||||
|
commit('addNewChats', { dispatch, chats, rootGetters })
|
||||||
|
},
|
||||||
|
updateChat ({ commit }, { chat }) {
|
||||||
|
commit('updateChat', { chat })
|
||||||
|
},
|
||||||
|
|
||||||
|
// Opened Chats
|
||||||
|
startFetchingCurrentChat ({ commit, dispatch }, { fetcher }) {
|
||||||
|
dispatch('setCurrentChatFetcher', { fetcher })
|
||||||
|
},
|
||||||
|
setCurrentChatFetcher ({ rootState, commit }, { fetcher }) {
|
||||||
|
commit('setCurrentChatFetcher', { fetcher })
|
||||||
|
},
|
||||||
|
addOpenedChat ({ rootState, commit, dispatch }, { chat }) {
|
||||||
|
commit('addOpenedChat', { dispatch, chat: parseChat(chat) })
|
||||||
|
dispatch('addNewUsers', [chat.account])
|
||||||
|
},
|
||||||
|
addChatMessages ({ commit }, value) {
|
||||||
|
commit('addChatMessages', { commit, ...value })
|
||||||
|
},
|
||||||
|
resetChatNewMessageCount ({ commit }, value) {
|
||||||
|
commit('resetChatNewMessageCount', value)
|
||||||
|
},
|
||||||
|
clearCurrentChat ({ rootState, commit, dispatch }, value) {
|
||||||
|
commit('setCurrentChatId', { chatId: undefined })
|
||||||
|
commit('setCurrentChatFetcher', { fetcher: undefined })
|
||||||
|
},
|
||||||
|
readChat ({ rootState, commit, dispatch }, { id, lastReadId }) {
|
||||||
|
dispatch('resetChatNewMessageCount')
|
||||||
|
commit('readChat', { id })
|
||||||
|
rootState.api.backendInteractor.readChat({ id, lastReadId })
|
||||||
|
},
|
||||||
|
deleteChatMessage ({ rootState, commit }, value) {
|
||||||
|
rootState.api.backendInteractor.deleteChatMessage(value)
|
||||||
|
commit('deleteChatMessage', { commit, ...value })
|
||||||
|
},
|
||||||
|
resetChats ({ commit, dispatch }) {
|
||||||
|
dispatch('clearCurrentChat')
|
||||||
|
commit('resetChats', { commit })
|
||||||
|
},
|
||||||
|
clearOpenedChats ({ rootState, commit, dispatch, rootGetters }) {
|
||||||
|
commit('clearOpenedChats', { commit })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setChatListFetcher (state, { commit, fetcher }) {
|
||||||
|
const prevFetcher = state.chatListFetcher
|
||||||
|
if (prevFetcher) {
|
||||||
|
clearInterval(prevFetcher)
|
||||||
|
}
|
||||||
|
state.chatListFetcher = fetcher && fetcher()
|
||||||
|
},
|
||||||
|
setCurrentChatFetcher (state, { fetcher }) {
|
||||||
|
const prevFetcher = state.fetcher
|
||||||
|
if (prevFetcher) {
|
||||||
|
clearInterval(prevFetcher)
|
||||||
|
}
|
||||||
|
state.fetcher = fetcher && fetcher()
|
||||||
|
},
|
||||||
|
addOpenedChat (state, { _dispatch, chat }) {
|
||||||
|
state.currentChatId = chat.id
|
||||||
|
Vue.set(state.openedChats, chat.id, chat)
|
||||||
|
|
||||||
|
if (!state.openedChatMessageServices[chat.id]) {
|
||||||
|
Vue.set(state.openedChatMessageServices, chat.id, chatService.empty(chat.id))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setCurrentChatId (state, { chatId }) {
|
||||||
|
state.currentChatId = chatId
|
||||||
|
},
|
||||||
|
addNewChats (state, { _dispatch, chats, _rootGetters }) {
|
||||||
|
chats.forEach((updatedChat) => {
|
||||||
|
const chat = getChatById(state, updatedChat.id)
|
||||||
|
|
||||||
|
if (chat) {
|
||||||
|
chat.lastMessage = updatedChat.lastMessage
|
||||||
|
chat.unread = updatedChat.unread
|
||||||
|
} else {
|
||||||
|
state.chatList.data.push(updatedChat)
|
||||||
|
Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateChat (state, { _dispatch, chat: updatedChat, _rootGetters }) {
|
||||||
|
const chat = getChatById(state, updatedChat.id)
|
||||||
|
if (chat) {
|
||||||
|
chat.lastMessage = updatedChat.lastMessage
|
||||||
|
chat.unread = updatedChat.unread
|
||||||
|
chat.updated_at = updatedChat.updated_at
|
||||||
|
}
|
||||||
|
if (!chat) { state.chatList.data.unshift(updatedChat) }
|
||||||
|
Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
|
||||||
|
},
|
||||||
|
deleteChat (state, { _dispatch, id, _rootGetters }) {
|
||||||
|
state.chats.data = state.chats.data.filter(conversation =>
|
||||||
|
conversation.last_status.id !== id
|
||||||
|
)
|
||||||
|
state.chats.idStore = omitBy(state.chats.idStore, conversation => conversation.last_status.id === id)
|
||||||
|
},
|
||||||
|
resetChats (state, { commit }) {
|
||||||
|
state.chatList = emptyChatList()
|
||||||
|
state.currentChatId = null
|
||||||
|
commit('setChatListFetcher', { fetcher: undefined })
|
||||||
|
for (const chatId in state.openedChats) {
|
||||||
|
chatService.clear(state.openedChatMessageServices[chatId])
|
||||||
|
Vue.delete(state.openedChats, chatId)
|
||||||
|
Vue.delete(state.openedChatMessageServices, chatId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setChatsLoading (state, { value }) {
|
||||||
|
state.chats.loading = value
|
||||||
|
},
|
||||||
|
addChatMessages (state, { commit, chatId, messages }) {
|
||||||
|
const chatMessageService = state.openedChatMessageServices[chatId]
|
||||||
|
if (chatMessageService) {
|
||||||
|
chatService.add(chatMessageService, { messages: messages.map(parseChatMessage) })
|
||||||
|
commit('refreshLastMessage', { chatId })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refreshLastMessage (state, { chatId }) {
|
||||||
|
const chatMessageService = state.openedChatMessageServices[chatId]
|
||||||
|
if (chatMessageService) {
|
||||||
|
const chat = getChatById(state, chatId)
|
||||||
|
if (chat) {
|
||||||
|
chat.lastMessage = chatMessageService.lastMessage
|
||||||
|
if (chatMessageService.lastMessage) {
|
||||||
|
chat.updated_at = chatMessageService.lastMessage.created_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteChatMessage (state, { commit, chatId, messageId }) {
|
||||||
|
const chatMessageService = state.openedChatMessageServices[chatId]
|
||||||
|
if (chatMessageService) {
|
||||||
|
chatService.deleteMessage(chatMessageService, messageId)
|
||||||
|
commit('refreshLastMessage', { chatId })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetChatNewMessageCount (state, _value) {
|
||||||
|
const chatMessageService = state.openedChatMessageServices[state.currentChatId]
|
||||||
|
chatService.resetNewMessageCount(chatMessageService)
|
||||||
|
},
|
||||||
|
// Used when a connection loss occurs
|
||||||
|
clearOpenedChats (state) {
|
||||||
|
const currentChatId = state.currentChatId
|
||||||
|
for (const chatId in state.openedChats) {
|
||||||
|
if (currentChatId !== chatId) {
|
||||||
|
chatService.clear(state.openedChatMessageServices[chatId])
|
||||||
|
Vue.delete(state.openedChats, chatId)
|
||||||
|
Vue.delete(state.openedChatMessageServices, chatId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
readChat (state, { id }) {
|
||||||
|
const chat = getChatById(state, id)
|
||||||
|
if (chat) {
|
||||||
|
chat.unread = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default chats
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue