Colubrina/frontend/src/views/Communications/CommunicationsChat.vue

1268 lines
38 KiB
Vue

<template>
<div id="communications-chat" @dragover.prevent @drop.prevent="handleDrag">
<v-menu
v-model="$store.state.context.pins.value"
:position-x="$store.state.context.pins.x"
:position-y="60"
class="rounded-l elevation-7"
absolute
transition="scroll-y-transition"
:close-on-content-click="false"
style="z-index: 15"
>
<v-card min-width="400" max-width="400" color="toolbar">
<v-toolbar color="toolbar lighten-1">
<v-spacer />
<v-toolbar-title> Pins </v-toolbar-title>
<v-spacer />
</v-toolbar>
<v-divider />
<v-container>
<v-list v-if="pins.length" dense :max-height="600">
<v-list-item
v-for="(pin, index) in pins"
:key="index"
@click="jumpToMessage(pin.message.id)"
>
<SimpleMessage
:key="pin.message.keyId"
:message="pin.message"
:index="index"
/>
<v-spacer />
<v-btn icon text @click.stop="removePin(pin.messageId)">
<v-icon> mdi-close </v-icon>
</v-btn>
</v-list-item>
</v-list>
<v-list-item v-else-if="pinsLoading">
<v-list-item-title> Loading... </v-list-item-title>
</v-list-item>
<v-list-item v-else>
<v-list-item-title> This chat has no pins yet. </v-list-item-title>
</v-list-item>
</v-container>
</v-card>
</v-menu>
<v-menu
v-model="context.message.value"
:position-x="context.message.x"
:position-y="context.message.y"
absolute
offset-y
class="rounded-l"
>
<v-list v-if="context.message.item" class="rounded-l">
<v-list-item @click="copy(context.message.item.content)">
<v-list-item-title>Copy Message Content</v-list-item-title>
</v-list-item>
<v-list-item @click="replying = context.message.item">
<v-list-item-title>Reply to Message</v-list-item-title>
</v-list-item>
<v-list-item
v-if="
context.message.item.userId === $store.state.user.id &&
edit.id !== context.message.item.id
"
@click="
edit.content = context.message.item.content
edit.editing = true
edit.id = context.message.item.id
"
>
<v-list-item-title>Edit Message</v-list-item-title>
</v-list-item>
<v-list-item
v-if="context.message.item.userId === $store.state.user.id"
@click="deleteMessage(context.message.item)"
>
<v-list-item-title>Delete Message</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<UserDialog
:key="context.userPopout.item?.id || 0"
:user="context.userPopout"
/>
<NicknameDialog :nickname="nickname" />
<v-dialog
v-model="preview.dialog"
elevation="0"
:min-height="300"
:max-width="1000"
:max-height="600"
content-class="rounded-0"
>
<v-card color="card">
<v-img
:src="preview.src"
:max-width="1000"
:max-height="600"
:min-height="300"
contain
/>
<v-container>
<a :href="preview.src" style="text-decoration: none" target="_blank">
<small> Open Externally </small>
</a>
<small
class="float-end"
style="text-decoration: none; color: inherit"
>
{{ preview.name }}
</small>
</v-container>
</v-card>
</v-dialog>
<v-navigation-drawer
v-if="$vuetify.breakpoint.mobile"
v-model="$store.state.userPanel"
color="bg"
floating
app
right
style="z-index: 100"
>
<v-list two-line color="card">
<v-list-item-group class="rounded-xl">
<template v-for="item in associations">
<v-list-item
:id="'user-popout-' + item.userId"
:key="item.title"
@contextmenu="show($event, 'user', item.user)"
@click="openUserPanel(item.user)"
>
<v-badge
bordered
bottom
:color="getStatus(item.user)"
dot
offset-x="24"
offset-y="26"
>
<v-list-item-avatar :color="$vuetify.theme.themes.dark.primary">
<v-img
v-if="item.user.avatar"
:src="
$store.state.baseURL + '/usercontent/' + item.user.avatar
"
/>
<v-icon v-else> mdi-account </v-icon>
</v-list-item-avatar>
</v-badge>
<template>
<v-list-item-content>
<v-list-item-title>
{{ getName(item.user) }}
</v-list-item-title>
</v-list-item-content>
</template>
</v-list-item>
</template>
</v-list-item-group>
</v-list>
</v-navigation-drawer>
<v-row no-gutters style="overflow: hidden" @drop="handleDrag">
<v-col
id="chat-col"
class="flex-grow-1 flex-shrink-1 pb-0"
style="overflow: hidden"
>
<v-card
class="d-flex flex-column-reverse fill-height rounded-0 mb-n3 chat-col"
style="overflow: auto; height: calc(100vh - 52px)"
color="card"
elevation="0"
>
<v-card-text>
<v-toolbar
v-if="replying"
elevation="0"
height="35"
color="card"
style="cursor: pointer; overflow: hidden"
@click="jumpToMessage(replying?.id)"
>
<v-icon class="mr-2"> mdi-reply </v-icon>
<v-avatar size="24" class="mr-2">
<v-img
v-if="replying.user.avatar"
:src="
$store.state.baseURL +
'/usercontent/' +
replying.user.avatar
"
class="elevation-1"
/>
<v-icon v-else class="elevation-1"> mdi-account </v-icon>
</v-avatar>
<template v-if="replying.attachments.length">
<v-icon class="mr-2"> mdi-file-image </v-icon>
</template>
<template v-if="!replying.content && replying.attachments.length">
Click to view attachment
</template>
{{ replying.content.substring(0, 100) }}
<v-spacer />
<v-btn icon class="mr-2" small @click="replying = null">
<v-icon> mdi-close </v-icon>
</v-btn>
</v-toolbar>
<v-fade-transition v-model="avoidAutoScroll">
<v-toolbar
v-if="avoidAutoScroll"
height="24"
color="toolbar"
elevation="0"
style="
border-radius: 20px 20px 0 0;
cursor: pointer;
position: relative;
top: -30px;
margin-bottom: -27px;
z-index: 20;
"
width="100%"
@click="forceScroll"
>
<div>
<v-icon size="16px"> mdi-arrow-down </v-icon>
Jump to bottom...
</div>
</v-toolbar>
</v-fade-transition>
<v-fade-transition
v-if="$vuetify.breakpoint.mobile"
v-model="usersTyping.length"
>
<div
v-if="usersTyping.length"
style="
border-radius: 0 0 20px 20px;
position: relative;
top: -30px;
margin-bottom: -22px;
"
>
{{ usersTyping.map((user) => getName(user)).join(", ") }}
{{ usersTyping.length > 1 ? " are" : " is" }} typing...
</div>
</v-fade-transition>
<CommsInput
:chat="chat"
:replying="replying"
:edit-last-message="editLastMessage"
:auto-scroll="autoScroll"
:end-send="endSend"
/>
<v-fade-transition
v-if="!$vuetify.breakpoint.mobile"
v-model="usersTyping.length"
>
<div
v-if="usersTyping.length"
style="
border-radius: 0 0 20px 20px;
position: absolute;
margin-top: -2px;
bottom: 1px;
"
>
{{ usersTyping.map((user) => getName(user)).join(", ") }}
{{ usersTyping.length > 1 ? " are" : " is" }} typing...
</div>
</v-fade-transition>
</v-card-text>
<v-card-text id="message-list" class="flex-grow-1 overflow-y-auto">
<v-card-title
v-if="
reachedTop && $store.state.selectedChat?.chat?.type === 'group'
"
>
Welcome to the start of
{{
$store.state.selectedChat?.chat?.type === "direct"
? getDirectRecipient($store.state.selectedChat).username
: $store.state.selectedChat?.chat?.name
}}
</v-card-title>
<v-card-title v-else-if="reachedTop">
Welcome to the start of the conversation with
{{
$store.state.selectedChat?.chat?.type === "direct"
? getDirectRecipient($store.state.selectedChat).username
: $store.state.selectedChat?.chat?.name
}}
</v-card-title>
<v-progress-circular
v-if="loadingMessages"
indeterminate
size="64"
style="display: block; width: 100px; margin: 0 auto"
/>
<template v-for="(message, index) in messages">
<div
v-if="message.readReceipts.length"
:key="'div2-' + message.keyId"
>
<v-tooltip
v-for="association in message.readReceipts"
:key="association.id"
top
>
<template
v-if="association.user.id !== $store.state.user.id"
#activator="{ on }"
>
<v-btn
icon
small
fab
width="20"
height="20"
class="ml-2 mt-2"
style="float: right"
@click="openUserPanel(association.user)"
>
<v-avatar size="20" color="primary" v-on="on">
<img
v-if="association.user.avatar"
:src="
$store.state.baseURL +
'/usercontent/' +
association.user.avatar
"
alt="avatar"
/>
<span v-else>{{
association.user.username[0].toUpperCase()
}}</span>
</v-avatar>
</v-btn>
</template>
<span>
{{ association.user.username }} has read up to this point.
</span>
</v-tooltip>
<br v-if="index + 1 > messages.length" />
<br v-if="index + 1 > messages.length" />
</div>
<Message
:key="message.keyId"
:message="message"
:jump-to-message="jumpToMessage"
:edit="edit"
:focus-input="focusInput"
:replying="setReply"
:get-name="getName"
:end-edit="endEdit"
:auto-scroll="autoScroll"
:chat="chat"
:index="index"
:show="show"
:set-image-preview="setImagePreview"
:delete-message="deleteMessage"
:last-message="
messages[index - 1]?.userId === message?.userId &&
$date(message.createdAt).diff(
messages[index - 1]?.createdAt,
'minute'
) < 10 &&
!message.replyId &&
!message.type
"
/>
</template>
</v-card-text>
</v-card>
</v-col>
<v-col
v-if="$store.state.searchPanel && !$vuetify.breakpoint.mobile"
id="search-col"
cols="3"
class=""
style="z-index: 15"
>
<v-card
class="d-flex flex-column fill-height"
style="overflow: scroll; height: calc(100vh - 24px - 40px)"
color="card"
elevation="0"
>
<v-toolbar color="toolbar" class="flex-grow-0 flex-shrink-0">
<v-toolbar-title>
Search ({{ search.pager.totalItems || 0 }})
</v-toolbar-title>
<v-spacer />
<v-btn icon @click="$store.state.searchPanel = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar>
<v-card-text class="flex-grow-1 overflow-y-auto">
<v-text-field
v-model="search.query"
label="Search"
outlined
autofocus
@keydown.enter="doSearch"
/>
<v-list ref="message-list-search" two-line color="card">
<template v-for="(message, index) in search.results">
<div
:key="message.keyId"
style="cursor: pointer"
@click="jumpToMessage(message.id)"
>
<Message
:message="message"
:jump-to-message="jumpToMessage"
:edit="edit"
:focus-input="focusInput"
:replying="setReply"
:get-name="getName"
:end-edit="endEdit"
:auto-scroll="autoScroll"
:chat="chat"
:index="index"
:show="show"
:set-image-preview="setImagePreview"
:delete-message="deleteMessage"
:last-message="false"
/>
</div>
</template>
<v-pagination
v-model="search.page"
class="my-4"
:length="search.pager.totalPages"
@input="doSearch"
/>
</v-list>
</v-card-text>
</v-card>
</v-col>
<v-col
v-if="
$store.state.userPanel &&
!$vuetify.breakpoint.mobile &&
!$store.state.searchPanel
"
id="user-col"
:cols="$vuetify.breakpoint.xl ? 2 : 3"
>
<v-card
class="d-flex flex-column fill-height rounded-0"
elevation="0"
style="overflow: scroll; height: calc(100vh - 24px - 40px)"
color="sheet"
>
<v-menu
v-model="context.user.value"
:position-x="context.user.x"
:position-y="context.user.y"
absolute
offset-y
class="rounded-l"
>
<v-list v-if="context.user.item" class="rounded-l">
<v-list-item
@click="
nickname.dialog = true
nickname.user = context.user.item
"
>
<v-list-item-title>
Change Friend Nickname for
{{ context.user.item.username }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-list two-line color="sheet">
<v-list-item-group class="rounded-xl">
<template v-for="item in associations">
<v-list-item
:id="'user-popout-' + item.userId"
:key="item.title"
@contextmenu="show($event, 'user', item.user)"
@click="openUserPanel(item.user)"
>
<v-badge
bordered
bottom
:color="getStatus(item.user)"
dot
offset-x="24"
offset-y="26"
>
<v-list-item-avatar
:color="$vuetify.theme.themes.dark.primary"
>
<v-img
v-if="item.user.avatar"
:src="
$store.state.baseURL +
'/usercontent/' +
item.user.avatar
"
/>
<v-icon v-else> mdi-account </v-icon>
</v-list-item-avatar>
</v-badge>
<template>
<v-list-item-content>
<v-list-item-title>
{{ getName(item.user) }}
<v-tooltip v-if="item.user.admin" top>
<template #activator="{ on }">
<v-btn icon small v-on="on">
<v-icon> mdi-crown </v-icon>
</v-btn>
</template>
<span>Colubrina Instance Administrator</span>
</v-tooltip>
<v-tooltip v-if="item.user.bot" top>
<template #activator="{ on }">
<v-btn icon small v-on="on">
<v-icon> mdi-robot </v-icon>
</v-btn>
</template>
<span>Bot</span>
</v-tooltip>
<v-tooltip v-if="item.user.id < 35" top>
<template #activator="{ on }">
<v-btn icon small v-on="on">
<v-icon> mdi-alpha-a-circle </v-icon>
</v-btn>
</template>
<span>Early User</span>
</v-tooltip>
</v-list-item-title>
</v-list-item-content>
</template>
</v-list-item>
</template>
</v-list-item-group>
</v-list>
<br />
</v-card>
</v-col>
</v-row>
</div>
</template>
<script>
import AjaxErrorHandler from "@/lib/errorHandler"
import CommsInput from "@/components/CommsInput"
import NicknameDialog from "@/components/NicknameDialog"
import UserDialog from "@/components/UserDialog"
import Message from "@/components/Message"
import SimpleMessage from "@/components/SimpleMessage"
export default {
name: "CommunicationsChat",
components: {
SimpleMessage,
Message,
UserDialog,
NicknameDialog,
CommsInput
},
data: () => ({
interval: null,
pins: [],
pinsLoading: true,
reachedTop: false,
graphOptions: {
responsive: true,
maintainAspectRatio: false,
title: {
display: false
}
},
offset: null,
nickname: {
dialog: false,
nickname: "",
user: {}
},
context: {
user: {
value: false,
x: null,
y: null,
item: null
},
userPopout: {
value: false,
x: null,
y: null,
item: null,
id: 0
},
message: {
value: false,
x: null,
y: null,
item: null
}
},
search: {
query: "",
results: [],
pager: {
totalPages: 1
},
loading: false,
page: 1
},
preview: {
dialog: false,
src: "",
height: 0,
width: 0,
name: ""
},
fileTypes: {
png: "mdi-file-image",
jpg: "mdi-file-image",
jpeg: "mdi-file-image",
gif: "mdi-file-image",
mp4: "mdi-file-video",
mp3: "mdi-file-music",
pdf: "mdi-file-pdf",
doc: "mdi-file-word",
docx: "mdi-file-word",
xls: "mdi-file-excel",
xlsx: "mdi-file-excel",
ppt: "mdi-file-powerpoint",
pptx: "mdi-file-powerpoint",
zip: "mdi-file-zip",
rar: "mdi-file-zip",
txt: "mdi-file-document",
csv: "mdi-file-spreadsheet",
html: "mdi-file-html",
htm: "mdi-file-html",
js: "mdi-file-code",
json: "mdi-file-code",
css: "mdi-file-css",
otf: "mdi-file-font",
ttf: "mdi-file-font",
woff: "mdi-file-font",
woff2: "mdi-file-font",
otf2: "mdi-file-font",
ttf2: "mdi-file-font",
eot: "mdi-file-font",
svg: "mdi-file-image",
ico: "mdi-file-image",
webp: "mdi-file-image",
other: "mdi-file",
xml: "mdi-file-code"
},
replying: null,
emojiPicker: false,
messages: [],
typingDate: null,
file: null,
message: "",
edit: {
content: "",
editing: false,
id: null
},
usersTyping: [],
blobURL: "",
autoScrollRetry: 0,
searchPanel: false,
userPanel: true,
rateLimit: false,
loadingMessages: true,
avoidAutoScroll: false,
lastRead: 0
}),
computed: {
chat() {
try {
return this.$store.state.chats.find(
(item) => item.id === parseInt(this.$route.params.id)
)
} catch {
return null
}
},
offsetValue() {
return this.offset || this.messages[0]?.id || 0
},
associations() {
if (this.chat) {
return this.chat.chat.associations.slice().sort((a, b) => {
if (a.lastRead > b.lastRead) {
return -1
} else if (a.lastRead < b.lastRead) {
return 1
} else {
return 0
}
})
} else {
console.log("Chat could not be found (associations)")
return []
}
}
},
watch: {
"$store.state.context.pins.value"(val) {
if (val) {
this.getPins()
}
},
userPanel() {
localStorage.setItem("userPanel", JSON.stringify(this.userPanel))
},
"$route.path"() {
const tryParse = this.$route.params.id
if (!tryParse) {
// remove event listeners
document.removeEventListener("keypress", this.focusKey)
document.removeEventListener("keydown", this.escPressed)
document
.getElementById("message-list")
.removeEventListener("scroll", this.scrollEvent)
clearInterval(this.interval)
}
},
"$route.params.id"(val, oldVal) {
this.focusInput()
let drafts = {}
if (localStorage.getItem("drafts")) {
drafts = JSON.parse(localStorage.getItem("drafts"))
}
if (this.message && drafts[oldVal]) {
drafts[oldVal] = this.message
localStorage.setItem("drafts", JSON.stringify(drafts))
} else if (!this.message && drafts[oldVal]) {
drafts[oldVal] = ""
}
this.message = drafts[val] || ""
this.usersTyping = []
this.replying = null
this.reachedTop = false
this.avoidAutoScroll = false
this.offset = null
this.pins = []
this.messages = []
this.getMessages()
}
},
mounted() {
this.$socket.on("memberListUpdate", () => {
this.$store.dispatch("getChats")
})
if (!this.$route.params.id) {
this.$router.push("/communications/friends")
return
}
document.addEventListener("keypress", this.focusKey)
document.addEventListener("keydown", this.escPressed)
document
.getElementById("message-list")
.addEventListener("scroll", this.scrollEvent)
this.interval = setInterval(() => {
this.typing()
if (
document.hasFocus() &&
this.messages[this.messages.length - 1]?.id !== this.lastRead
) {
this.markRead()
}
}, 1000)
this.getMessages()
if (localStorage.getItem("userPanel")) {
this.userPanel = JSON.parse(localStorage.getItem("userPanel"))
} else {
localStorage.setItem("userPanel", true)
}
let drafts = {}
if (localStorage.getItem("drafts")) {
drafts = JSON.parse(localStorage.getItem("drafts"))
}
if (drafts[this.$route.params.id]) {
this.message = drafts[this.$route.params.id]
}
this.$socket.on("readChat", (data) => {
if (!this.chat) return
if (data.id === this.chat?.id) {
this.lastRead = data.lastRead
}
})
this.$socket.on("readReceipt", (data) => {
if (!this.chat) return
try {
if (
data.messageId &&
data.chatId === this.chat.chatId &&
this.messages?.length
) {
this.messages.forEach((message) => {
message.readReceipts = message.readReceipts.filter(
(readReceipt) => readReceipt.id !== data.id
)
})
this.messages
.find((message) => message.id === data.messageId)
.readReceipts?.push(data)
this.autoScroll()
}
} catch (e) {
console.log("Read receipt error", e)
}
})
this.$socket.on("message", (message) => {
try {
if (!this.chat) return
if (message.chatId === this.chat.chatId) {
this.messages.push(message)
this.autoScroll()
if (document.hasFocus()) {
this.markRead()
}
if (this.messages.length > 50 && !this.avoidAutoScroll) {
this.messages.shift()
this.reachedTop = false
}
}
} catch (e) {
console.log("Message error", e)
}
})
this.$socket.on("editMessage", (message) => {
if (message.chatId === this.chat.chatId) {
const index = this.messages.findIndex((item) => item.id === message.id)
if (index !== -1) {
this.messages[index].content = message.content
this.messages[index].edited = message.edited
this.messages[index].editedAt = message.editedAt
this.messages[index].keyId = message.id + "-" + message.editedAt
}
}
})
this.$socket.on("messageEmbedResolved", (message) => {
if (message.chatId === this.chat.chatId) {
const index = this.messages.findIndex((item) => item.id === message.id)
if (index !== -1) {
this.messages[index].keyId = message.id + "-" + message.editedAt
this.messages[index].embeds = message.embeds
this.autoScroll()
}
}
})
this.$socket.on("typing", (event) => {
if (event.chatId === this.chat.chatId) {
const index = this.usersTyping.findIndex(
(item) => item.userId === event.userId
)
if (index > -1) {
this.usersTyping.splice(index, 1)
}
this.usersTyping.push(event)
}
})
this.$socket.on("deleteMessage", (message) => {
if (message.chatId === this.chat.chatId) {
const index = this.messages.findIndex((item) => item.id === message.id)
if (index !== -1) {
this.messages.splice(index, 1)
}
}
})
},
destroyed() {
document.removeEventListener("keypress", this.focusKey)
document.removeEventListener("keydown", this.escPressed)
document.removeEventListener("scroll", this.scrollEvent)
clearInterval(this.interval)
},
methods: {
copy(content) {
navigator.clipboard.writeText(content)
},
removePin(id) {
this.axios
.post(`/api/v1/communications/${this.chat.id}/pins`, {
messageId: id
})
.then(() => {
this.getPins()
})
.catch((e) => {
AjaxErrorHandler(this.$store)(e)
})
},
getPins() {
this.pinsLoading = true
this.axios
.get(
process.env.VUE_APP_BASE_URL +
"/api/v1/communications/" +
this.$route.params.id +
"/pins"
)
.then((res) => {
this.pins = res.data
this.pinsLoading = false
})
.catch((e) => {
AjaxErrorHandler(this.$store)(e)
})
},
forceScroll() {
this.avoidAutoScroll = false
this.autoScroll()
},
getDirectRecipient(item) {
let user = item.chat.users.find(
(user) => user.id !== this.$store.state.user.id
)
if (user) {
if (user.nickname?.nickname) {
user.name = user.nickname.nickname
} else {
user.name = user.username
}
return {
...user,
type: item.chat.type
}
} else {
let user = item.chat.users[0]
if (user.nickname?.nickname) {
user.name = user.nickname.nickname
} else {
user.name = user.username
}
return {
...user,
type: item.chat.type
}
}
},
async scrollEvent(e) {
this.avoidAutoScroll =
e.target.scrollHeight -
Math.round(e.target.scrollTop + e.target.offsetHeight) >
50
if (
e.target.scrollTop === 0 &&
!this.rateLimit &&
!this.reachedTop &&
!this.loadingMessages
) {
this.rateLimit = true
this.loadingMessages = true
const element = document.getElementById("message-0")
await this.getMessages()
if (element) {
element.scrollIntoView()
}
setTimeout(() => {
this.rateLimit = false
}, 250)
}
},
setReply(message) {
this.replying = message
},
markAsRead() {
try {
const unread = this.$store.state.chats.find(
(item) => item.id === JSON.parse(this.$route.params.id)
).unread
this.$store.state.chats.find(
(item) => item.id === JSON.parse(this.$route.params.id)
).unread = 0
this.$store.state.communicationNotifications -= unread
} catch {
console.log("Chat could not be found (markAsRead)")
}
},
endSend() {
this.replying = null
},
focusInput() {
const input = document.getElementById("message-input")
if (input) {
input.focus()
}
},
openUserPanel(user) {
this.context.userPopout.item = user
this.context.userPopout.value = true
},
getName(user) {
if (user.nickname?.nickname) {
return user.nickname.nickname
} else {
return user.username
}
},
setFriendNickname() {
this.axios
.post(
process.env.VUE_APP_BASE_URL +
"/api/v1/communications/nickname/" +
this.context.user.item.id,
{
nickname: this.nickname.nickname
}
)
.then((res) => {
this.context.user.value = false
this.nickname.dialog = false
this.nickname.nickname = ""
this.nickname.user = {}
this.$toast.success("Nickname changed successfully.")
this.items.forEach((item) => {
item.chat.associations.forEach((a) => {
if (a.user.id === this.context.user.item.id) {
a.user.nickname = {
nickname: res.data.nickname
}
}
})
item.chat.users.forEach((u) => {
if (u.id === this.context.user.item.id) {
u.nickname = {
nickname: res.data.nickname
}
}
})
})
})
.catch((e) => {
AjaxErrorHandler(this.$store)(e)
})
},
show(e, context, item) {
e.preventDefault()
this.context[context].value = false
this.context[context].x = e.clientX
this.context[context].y = e.clientY
this.context[context].item = item
this.context[context].id = item.id
this.$nextTick(() => {
this.context[context].value = true
})
},
getStatus(item) {
if (item.status === "online") {
return "green"
} else if (item.status === "offline") {
return "grey"
} else if (item.status === "away") {
return "orange"
} else if (item.status === "busy") {
return "red"
} else {
return "grey"
}
},
doSearch() {
if (this.search.query.length) {
this.axios
.get(
process.env.VUE_APP_BASE_URL +
"/api/v1/communications/" +
this.$route.params.id +
"/search",
{
params: {
query: this.search.query,
page: this.search.page
}
}
)
.then((res) => {
this.search.results = res.data.messages
this.search.pager = res.data.pager
})
.catch((e) => {
AjaxErrorHandler(this.$store)(e)
})
}
},
setImagePreview(attachment) {
const link = attachment.attachment
? this.$store.state.baseURL + "/usercontent/" + attachment.attachment
: attachment.mediaProxyLink
this.preview.src = link
const img = new Image()
img.onload = () => {
this.preview.height = img.height
this.preview.width = img.width
this.preview.dialog = true
this.preview.name = attachment.name
}
img.src = link
},
handleDrag(e) {
if (e.dataTransfer.files.length) {
this.file = e.dataTransfer.files[0]
}
},
friendlySize(size) {
if (size < 1024) {
return size + " bytes"
} else if (size < 1048576) {
return (size / 1024).toFixed(2) + " KB"
} else if (size < 1073741824) {
return (size / 1048576).toFixed(2) + " MB"
} else {
return (size / 1073741824).toFixed(2) + " GB"
}
},
deleteMessage(message) {
this.axios
.delete(
process.env.VUE_APP_BASE_URL +
"/api/v1/communications/" +
this.$route.params.id +
"/message/" +
message.id
)
.then(() => {
const index = this.messages.findIndex(
(item) => item.id === message.id
)
if (index !== -1) {
this.messages.splice(index, 1)
}
})
.catch((e) => {
AjaxErrorHandler(this.$store)(e)
})
},
async jumpToMessage(id) {
try {
console.log("Jumping to message", id)
const index = this.messages.findIndex((message) => message.id === id)
const lastMessage = document.querySelector(`#message-${index}`)
if (lastMessage) {
lastMessage.scrollIntoView({
behavior: "smooth"
})
// indicate message by changing background color to a blue
lastMessage.style.backgroundColor = "rgba(8,192,238,0.1)"
// set opacity of background color to 0.5
setTimeout(() => {
lastMessage.style.backgroundColor = ""
}, 1500)
} else {
this.offset = id + 10
await this.getMessages()
this.jumpToMessage(id)
this.offset = null
}
} catch (e) {
console.log(e)
console.log("Could not auto scroll (Jump to message)")
}
},
typing() {
this.usersTyping = this.usersTyping.filter((user) => {
return this.$date().isBefore(user.timeout)
})
},
editLastMessage() {
// find last message sent by current user
const lastMessage = this.messages
.slice()
.reverse()
.find((message) => message.userId === this.$store.state.user.id)
if (lastMessage) {
this.edit.content = lastMessage.content
this.edit.editing = true
this.edit.id = lastMessage.id
}
},
endEdit() {
this.edit.editing = false
this.edit.content = ""
this.edit.id = ""
this.focusInput()
},
autoScroll() {
this.$nextTick(() => {
if (!this.avoidAutoScroll) {
try {
const lastIndex = this.messages.length - 1
const lastMessage = document.querySelector(`#message-${lastIndex}`)
lastMessage.scrollIntoView()
this.autoScrollRetry = 0
} catch (e) {
console.log("Could not auto scroll, retrying...")
if (this.autoScrollRetry < 20) {
setTimeout(() => {
this.autoScroll()
}, 50)
this.autoScrollRetry++
} else {
console.log("Could not auto scroll, retry limit reached")
}
}
}
})
},
async getMessages() {
this.loadingMessages = true
this.autoScroll()
await this.axios
.get(
process.env.VUE_APP_BASE_URL +
"/api/v1/communications/" +
this.$route.params.id +
"/messages?limit=50&offset=" +
this.offsetValue
)
.then((res) => {
if (!res.data.length) {
this.reachedTop = true
}
this.messages.unshift(...res.data)
/* this.$store.commit("setMessages", {
id: this.chat.id,
messages: this.messages
})*/
this.loadingMessages = false
this.markRead()
this.$nextTick(() => {
this.autoScroll()
})
})
.catch((e) => {
AjaxErrorHandler(this.$store)(e)
})
},
markRead() {
this.axios.put(
process.env.VUE_APP_BASE_URL +
"/api/v1/communications/" +
this.$route.params.id +
"/read"
)
this.markAsRead()
},
focusKey() {
if (document.activeElement.tagName === "BODY") {
this.focusInput()
}
},
escPressed(event) {
if (event.key === "Escape" && !this.$store.state.searchPanel) {
this.forceScroll()
} else if (event.key === "Escape" && this.$store.state.searchPanel) {
this.$store.state.searchPanel = false
}
}
}
}
</script>