mirror of https://github.com/Troplo/Colubrina.git
1268 lines
38 KiB
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>
|