mirror of
https://github.com/Troplo/Colubrina.git
synced 2024-11-22 11:17:56 +11:00
1.0.9 [Pins & Read Receipts]
This commit is contained in:
parent
19bca17ba0
commit
ab4c172899
12 changed files with 1397 additions and 678 deletions
|
@ -25,8 +25,8 @@ Colubrina is a simple self-hostable chatting platform written in Vue, and Vuetif
|
|||
- [x] Mobile responsiveness/compatibility
|
||||
- [x] Email verification
|
||||
- [ ] Password resetting
|
||||
- [ ] Channel message pins
|
||||
- [ ] Read receipts
|
||||
- [x] Channel message pins
|
||||
- [x] Read receipts
|
||||
|
||||
<img src="https://i.troplo.com/i/d608273e066c.png" alt="Chat" width="45%"></img>
|
||||
<img src="https://i.troplo.com/i/e8e2c9d6e349.png" alt="Friends" width="45%"></img>
|
||||
|
|
42
backend/migrations/20220731050926-pins.js
Normal file
42
backend/migrations/20220731050926-pins.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
"use strict"
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.createTable("Pins", {
|
||||
id: {
|
||||
type: Sequelize.BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
pinnedById: {
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: false
|
||||
},
|
||||
messageId: {
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: false
|
||||
},
|
||||
chatId: {
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: false
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
/**
|
||||
* Add reverting commands here.
|
||||
*
|
||||
* Example:
|
||||
* await queryInterface.dropTable('users');
|
||||
*/
|
||||
}
|
||||
}
|
|
@ -22,6 +22,10 @@ module.exports = (sequelize, DataTypes) => {
|
|||
as: "attachments",
|
||||
foreignKey: "messageId"
|
||||
})
|
||||
Message.hasMany(models.ChatAssociation, {
|
||||
as: "readReceipts",
|
||||
foreignKey: "lastRead"
|
||||
})
|
||||
}
|
||||
}
|
||||
Message.init(
|
||||
|
|
51
backend/models/pins.js
Normal file
51
backend/models/pins.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
"use strict"
|
||||
const { Model } = require("sequelize")
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
class Pin extends Model {
|
||||
/**
|
||||
* Helper method for defining associations.
|
||||
* This method is not a part of Sequelize lifecycle.
|
||||
* The `models/index` file will call this method automatically.
|
||||
*/
|
||||
static associate(models) {
|
||||
Pin.belongsTo(models.User, {
|
||||
as: "pinnedBy"
|
||||
})
|
||||
Pin.belongsTo(models.Message, {
|
||||
as: "message"
|
||||
})
|
||||
Pin.belongsTo(models.Chat, {
|
||||
as: "chat"
|
||||
})
|
||||
}
|
||||
}
|
||||
Pin.init(
|
||||
{
|
||||
pinnedById: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false
|
||||
},
|
||||
messageId: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false
|
||||
},
|
||||
chatId: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: "Pin"
|
||||
}
|
||||
)
|
||||
return Pin
|
||||
}
|
|
@ -6,6 +6,7 @@ const {
|
|||
User,
|
||||
Chat,
|
||||
ChatAssociation,
|
||||
Pin,
|
||||
Message,
|
||||
Friend,
|
||||
Attachment,
|
||||
|
@ -66,6 +67,18 @@ async function createMessage(req, type, content, association, userId) {
|
|||
id: message.id
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: ChatAssociation,
|
||||
as: "readReceipts",
|
||||
attributes: ["id"],
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: "user",
|
||||
attributes: ["username", "name", "avatar", "id"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: Attachment,
|
||||
as: "attachments"
|
||||
|
@ -124,7 +137,6 @@ async function createMessage(req, type, content, association, userId) {
|
|||
]
|
||||
})
|
||||
associations.forEach((user) => {
|
||||
console.log(user)
|
||||
io.to(user.dataValues.userId).emit("message", {
|
||||
...messageLookup.dataValues,
|
||||
associationId: user.dataValues.id,
|
||||
|
@ -196,9 +208,7 @@ router.get("/", auth, async (req, res, next) => {
|
|||
"id",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
|
||||
"status",
|
||||
|
||||
"admin",
|
||||
"status",
|
||||
"bot"
|
||||
|
@ -468,6 +478,170 @@ router.get("/:id", auth, async (req, res, next) => {
|
|||
}
|
||||
})
|
||||
|
||||
router.get("/:id/pins", auth, async (req, res, next) => {
|
||||
try {
|
||||
let chat = await ChatAssociation.findOne({
|
||||
where: {
|
||||
userId: req.user.id,
|
||||
id: req.params.id
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Chat,
|
||||
as: "chat"
|
||||
}
|
||||
]
|
||||
})
|
||||
if (chat) {
|
||||
const pins = await Pin.findAll({
|
||||
where: {
|
||||
chatId: chat.chat.id
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: "pinnedBy",
|
||||
required: false,
|
||||
attributes: [
|
||||
"username",
|
||||
"name",
|
||||
"avatar",
|
||||
"id",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
],
|
||||
include: [
|
||||
{
|
||||
model: Nickname,
|
||||
as: "nickname",
|
||||
attributes: ["nickname"],
|
||||
required: false,
|
||||
where: {
|
||||
userId: req.user.id
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: Message,
|
||||
as: "message",
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: "user",
|
||||
attributes: [
|
||||
"username",
|
||||
"name",
|
||||
"avatar",
|
||||
"id",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
],
|
||||
include: [
|
||||
{
|
||||
model: Nickname,
|
||||
as: "nickname",
|
||||
attributes: ["nickname"],
|
||||
required: false,
|
||||
where: {
|
||||
userId: req.user.id
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
order: [["id", "DESC"]]
|
||||
})
|
||||
res.json(
|
||||
pins.map((pin) => {
|
||||
const message = pin.dataValues.message.dataValues
|
||||
return {
|
||||
...pin.dataValues,
|
||||
message: {
|
||||
...pin.dataValues.message.dataValues,
|
||||
keyId: `${message.id}-${message.updatedAt.toISOString()}`
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
} else {
|
||||
throw Errors.invalidParameter("chat association id")
|
||||
}
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post("/:id/pins", auth, async (req, res, next) => {
|
||||
try {
|
||||
const io = req.app.get("io")
|
||||
const chat = await ChatAssociation.findOne({
|
||||
where: {
|
||||
userId: req.user.id,
|
||||
id: req.params.id
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Chat,
|
||||
as: "chat",
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: "users",
|
||||
attributes: ["id"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
if (chat?.chat?.type === "direct" || chat?.rank === "admin") {
|
||||
const message = await Message.findOne({
|
||||
where: {
|
||||
id: req.body.messageId,
|
||||
chatId: chat.chat.id
|
||||
}
|
||||
})
|
||||
if (message) {
|
||||
const checkPin = await Pin.findOne({
|
||||
where: {
|
||||
messageId: message.id,
|
||||
chatId: chat.chat.id
|
||||
}
|
||||
})
|
||||
if (checkPin) {
|
||||
await checkPin.destroy()
|
||||
res.json({
|
||||
message: "Message unpinned successfully."
|
||||
})
|
||||
return
|
||||
}
|
||||
const pin = await Pin.create({
|
||||
chatId: chat.chat.id,
|
||||
messageId: req.body.messageId,
|
||||
pinnedById: req.user.id
|
||||
})
|
||||
await createMessage(
|
||||
req,
|
||||
"pin",
|
||||
`${req.user.username} pinned a message to the chat.`,
|
||||
chat,
|
||||
req.user.id
|
||||
)
|
||||
res.json({
|
||||
...pin.dataValues,
|
||||
message: "Message pinned successfully."
|
||||
})
|
||||
}
|
||||
} else {
|
||||
throw Errors.invalidParameter("chat association id")
|
||||
}
|
||||
} catch (e) {
|
||||
next(e)
|
||||
}
|
||||
})
|
||||
|
||||
router.put("/:id/read", auth, async (req, res, next) => {
|
||||
try {
|
||||
const io = req.app.get("io")
|
||||
|
@ -481,6 +655,11 @@ router.put("/:id/read", auth, async (req, res, next) => {
|
|||
model: Chat,
|
||||
as: "chat",
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: "users",
|
||||
attributes: ["id"]
|
||||
},
|
||||
{
|
||||
model: Message,
|
||||
as: "lastMessages",
|
||||
|
@ -501,6 +680,20 @@ router.put("/:id/read", auth, async (req, res, next) => {
|
|||
lastRead: chat.chat.lastMessages[0]?.id || null
|
||||
})
|
||||
res.sendStatus(204)
|
||||
for (const user of chat.chat.users) {
|
||||
io.to(user.id).emit("readReceipt", {
|
||||
id: chat.id,
|
||||
messageId: chat.chat.lastMessages[0]?.id || null,
|
||||
userId: req.user.id,
|
||||
chatId: chat.chat.id,
|
||||
user: {
|
||||
username: req.user.username,
|
||||
avatar: req.user.avatar,
|
||||
id: req.user.id
|
||||
},
|
||||
previousMessageId: chat.lastRead
|
||||
})
|
||||
}
|
||||
} else {
|
||||
throw Errors.invalidParameter("chat association id")
|
||||
}
|
||||
|
@ -877,6 +1070,18 @@ router.post(
|
|||
id: message.id
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: ChatAssociation,
|
||||
as: "readReceipts",
|
||||
attributes: ["id"],
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: "user",
|
||||
attributes: ["username", "name", "avatar", "id"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: Attachment,
|
||||
as: "attachments"
|
||||
|
@ -1130,6 +1335,18 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => {
|
|||
id: message.id
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: ChatAssociation,
|
||||
as: "readReceipts",
|
||||
attributes: ["id"],
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: "user",
|
||||
attributes: ["username", "name", "avatar", "id"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: Attachment,
|
||||
as: "attachments"
|
||||
|
@ -1399,6 +1616,18 @@ router.get("/:id/messages", auth, async (req, res, next) => {
|
|||
...or
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: ChatAssociation,
|
||||
as: "readReceipts",
|
||||
attributes: ["id"],
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: "user",
|
||||
attributes: ["username", "name", "avatar", "id"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: Attachment,
|
||||
as: "attachments"
|
||||
|
@ -1409,12 +1638,10 @@ router.get("/:id/messages", auth, async (req, res, next) => {
|
|||
attributes: [
|
||||
"username",
|
||||
"name",
|
||||
|
||||
"avatar",
|
||||
"id",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
|
||||
"bot"
|
||||
],
|
||||
include: [
|
||||
|
@ -1439,12 +1666,10 @@ router.get("/:id/messages", auth, async (req, res, next) => {
|
|||
attributes: [
|
||||
"username",
|
||||
"name",
|
||||
|
||||
"avatar",
|
||||
"id",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
|
||||
"bot"
|
||||
]
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "colubrina-chat",
|
||||
"version": "1.0.8",
|
||||
"version": "1.0.9",
|
||||
"private": true,
|
||||
"author": "Troplo <troplo@troplo.com>",
|
||||
"license": "GPL-3.0",
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
.message-hover {
|
||||
background-color: var(--v-bg-lighten1);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.message-toast {
|
||||
background-color: rgba(47, 47, 47, 0.9) !important;
|
||||
}
|
||||
|
|
|
@ -338,6 +338,14 @@
|
|||
}}
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
icon
|
||||
v-model="$store.state.context.pins.value"
|
||||
@click="show($event, 'pins', null, null, true)"
|
||||
id="pin-button"
|
||||
>
|
||||
<v-icon>mdi-pin-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon
|
||||
@click="$store.state.searchPanel = !$store.state.searchPanel"
|
||||
|
@ -345,7 +353,7 @@
|
|||
<v-icon>mdi-magnify</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon @click="$store.state.userPanel = !$store.state.userPanel">
|
||||
<v-icon>mdi-account-group</v-icon>
|
||||
<v-icon>mdi-account-group-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
@ -636,7 +644,8 @@ export default {
|
|||
value: false,
|
||||
x: null,
|
||||
y: null,
|
||||
item: null
|
||||
item: null,
|
||||
id: 0
|
||||
}
|
||||
},
|
||||
selected: [2],
|
||||
|
@ -684,16 +693,28 @@ export default {
|
|||
this.leave.item = this.$store.state.chats.find((chat) => chat.id === id)
|
||||
this.leave.dialog = true
|
||||
},
|
||||
show(e, context, item, id) {
|
||||
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 = id
|
||||
this.$nextTick(() => {
|
||||
this.context[context].value = true
|
||||
})
|
||||
show(e, context, item, id, state) {
|
||||
if (!state) {
|
||||
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 = id
|
||||
this.$nextTick(() => {
|
||||
this.context[context].value = true
|
||||
})
|
||||
} else {
|
||||
e.preventDefault()
|
||||
this.$store.state.context[context].value = false
|
||||
this.$store.state.context[context].x = e.clientX
|
||||
this.$store.state.context[context].y = e.clientY
|
||||
this.$store.state.context[context].item = item
|
||||
this.$store.state.context[context].id = id
|
||||
this.$nextTick(() => {
|
||||
this.$store.state.context[context].value = true
|
||||
})
|
||||
}
|
||||
},
|
||||
setStatus(status) {
|
||||
const previousStatus = {
|
||||
|
|
File diff suppressed because it is too large
Load diff
106
frontend/src/components/SimpleMessage.vue
Normal file
106
frontend/src/components/SimpleMessage.vue
Normal file
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<div v-if="!message.type" class="rounded-l">
|
||||
<v-toolbar
|
||||
@click="jumpToMessage(message.replyId)"
|
||||
:key="message.keyId + '-reply-toolbar'"
|
||||
elevation="0"
|
||||
height="40"
|
||||
color="card"
|
||||
v-if="message.reply"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
<v-icon class="mr-2">mdi-reply</v-icon>
|
||||
<v-avatar size="24" class="mr-2">
|
||||
<v-img
|
||||
:src="
|
||||
$store.state.baseURL + '/usercontent/' + message.reply.user.avatar
|
||||
"
|
||||
v-if="message.reply.user.avatar"
|
||||
class="elevation-1"
|
||||
/>
|
||||
<v-icon v-else class="elevation-1"> mdi-account </v-icon>
|
||||
</v-avatar>
|
||||
<template v-if="message.reply.attachments.length">
|
||||
<v-icon class="mr-2">mdi-file-image</v-icon>
|
||||
</template>
|
||||
<template
|
||||
v-if="!message.reply.content && message.reply.attachments.length"
|
||||
>
|
||||
Click to view attachment
|
||||
</template>
|
||||
{{ message.reply.content.substring(0, 100) }}
|
||||
</v-toolbar>
|
||||
<v-list-item
|
||||
:key="message.keyId"
|
||||
:class="{
|
||||
'text-xs-right': message.userId === $store.state.user.id,
|
||||
'text-xs-left': message.userId !== $store.state.user.id
|
||||
}"
|
||||
:id="'message-' + index"
|
||||
>
|
||||
<v-avatar size="48" class="mr-2">
|
||||
<v-img
|
||||
:src="$store.state.baseURL + '/usercontent/' + message.user.avatar"
|
||||
v-if="message.user.avatar"
|
||||
class="elevation-1"
|
||||
/>
|
||||
<v-icon v-else class="elevation-1"> mdi-account </v-icon>
|
||||
</v-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle>
|
||||
{{ getName(message.user) }}
|
||||
<v-chip v-if="message.user.bot" color="calendarNormalActivity" small>
|
||||
<v-icon small>mdi-robot</v-icon>
|
||||
</v-chip>
|
||||
<small>
|
||||
{{ $date(message.createdAt).format("hh:mm A, DD/MM/YYYY") }}
|
||||
</small>
|
||||
<v-tooltip top v-if="message.edited">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<span v-on="on" v-bind="attrs">
|
||||
<v-icon
|
||||
color="grey"
|
||||
small
|
||||
style="
|
||||
margin-bottom: 2px;
|
||||
margin-left: 4px;
|
||||
position: absolute;
|
||||
"
|
||||
>
|
||||
mdi-pencil
|
||||
</v-icon>
|
||||
</span>
|
||||
</template>
|
||||
<span>
|
||||
{{ $date(message.editedAt).format("DD/MM/YYYY hh:mm:ss A") }}
|
||||
</span>
|
||||
</v-tooltip>
|
||||
</v-list-item-subtitle>
|
||||
<p v-markdown style="overflow-wrap: anywhere">
|
||||
{{ message.content }}
|
||||
</p>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SimpleMessage",
|
||||
props: ["message", "index"],
|
||||
methods: {
|
||||
jumpToMessage(id) {
|
||||
this.$store.dispatch("jumpToMessage", id)
|
||||
},
|
||||
getName(user) {
|
||||
if (user.nickname?.nickname) {
|
||||
return user.nickname.nickname
|
||||
} else {
|
||||
return user.username
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -35,6 +35,13 @@ export default new Vuex.Store({
|
|||
version: process.env.VUE_APP_VERSION,
|
||||
release: process.env.RELEASE
|
||||
},
|
||||
context: {
|
||||
pins: {
|
||||
x: null,
|
||||
y: null,
|
||||
value: false
|
||||
}
|
||||
},
|
||||
drawer: true,
|
||||
site: {
|
||||
release: "stable",
|
||||
|
|
|
@ -1,5 +1,47 @@
|
|||
<template>
|
||||
<div id="communications-chat" @dragover.prevent @drop.prevent>
|
||||
<v-menu
|
||||
:position-x="$store.state.context.pins.x"
|
||||
:position-y="60"
|
||||
v-model="$store.state.context.pins.value"
|
||||
class="rounded-l"
|
||||
absolute
|
||||
transition="scroll-y-transition"
|
||||
:close-on-content-click="false"
|
||||
>
|
||||
<v-card min-width="350" color="toolbar">
|
||||
<v-toolbar color="toolbar lighten-1">
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-title> Pins </v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</v-toolbar>
|
||||
<v-divider></v-divider>
|
||||
<v-container>
|
||||
<v-list dense v-if="pins.length">
|
||||
<v-list-item
|
||||
@click.self="jumpToMessage(pin.message.id)"
|
||||
v-for="(pin, index) in pins"
|
||||
:key="index"
|
||||
>
|
||||
<SimpleMessage
|
||||
:message="pin.message"
|
||||
:index="index"
|
||||
:key="pin.message.keyId"
|
||||
></SimpleMessage>
|
||||
<v-btn icon text @click="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"
|
||||
|
@ -126,7 +168,11 @@
|
|||
elevation="0"
|
||||
>
|
||||
<v-card-text class="flex-grow-1 overflow-y-auto" id="message-list">
|
||||
<v-card-title v-if="reachedTop">
|
||||
<v-card-title
|
||||
v-if="
|
||||
reachedTop && $store.state.selectedChat?.chat?.type === 'group'
|
||||
"
|
||||
>
|
||||
Welcome to the start of
|
||||
{{
|
||||
$store.state.selectedChat?.chat?.type === "direct"
|
||||
|
@ -134,6 +180,14 @@
|
|||
: $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
|
||||
|
@ -141,22 +195,63 @@
|
|||
style="display: block; width: 100px; margin: 0 auto"
|
||||
></v-progress-circular>
|
||||
<template v-for="(message, index) in messages">
|
||||
<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"
|
||||
></Message>
|
||||
<div :key="'div-' + message.keyId">
|
||||
<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"
|
||||
></Message>
|
||||
</div>
|
||||
<div
|
||||
:key="'div2-' + message.keyId"
|
||||
v-if="message.readReceipts.length"
|
||||
>
|
||||
<v-tooltip
|
||||
v-for="association in message.readReceipts"
|
||||
:key="association.id"
|
||||
top
|
||||
>
|
||||
<template v-slot: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" v-on="on" color="primary">
|
||||
<img
|
||||
v-if="association.user.avatar"
|
||||
:src="'/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 />
|
||||
<br v-if="index + 1 <= messages.length" />
|
||||
</div>
|
||||
</template>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
|
@ -716,12 +811,21 @@ 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: { Message, UserDialog, NicknameDialog, CommsInput },
|
||||
components: {
|
||||
SimpleMessage,
|
||||
Message,
|
||||
UserDialog,
|
||||
NicknameDialog,
|
||||
CommsInput
|
||||
},
|
||||
props: ["chat", "loading", "items"],
|
||||
data: () => ({
|
||||
pins: [],
|
||||
pinsLoading: true,
|
||||
reachedTop: false,
|
||||
graphOptions: {
|
||||
responsive: true,
|
||||
|
@ -824,7 +928,8 @@ export default {
|
|||
userPanel: true,
|
||||
rateLimit: false,
|
||||
loadingMessages: true,
|
||||
avoidAutoScroll: false
|
||||
avoidAutoScroll: false,
|
||||
lastRead: 0
|
||||
}),
|
||||
computed: {
|
||||
associations() {
|
||||
|
@ -845,6 +950,35 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
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)
|
||||
})
|
||||
},
|
||||
forceBottom() {
|
||||
this.avoidAutoScroll = false
|
||||
this.autoScroll()
|
||||
|
@ -1198,6 +1332,12 @@ export default {
|
|||
.addEventListener("scroll", this.scrollEvent)
|
||||
setInterval(() => {
|
||||
this.typing()
|
||||
if (
|
||||
document.hasFocus() &&
|
||||
this.messages[this.messages.length - 1]?.id !== this.lastRead
|
||||
) {
|
||||
this.markRead()
|
||||
}
|
||||
}, 1000)
|
||||
this.getMessages()
|
||||
if (localStorage.getItem("userPanel")) {
|
||||
|
@ -1212,11 +1352,35 @@ export default {
|
|||
if (drafts[this.$route.params.id]) {
|
||||
this.message = drafts[this.$route.params.id]
|
||||
}
|
||||
this.$socket.on("readChat", (data) => {
|
||||
if (data.id === this.chat.id) {
|
||||
this.lastRead = data.lastRead
|
||||
}
|
||||
})
|
||||
this.$socket.on("readReceipt", (data) => {
|
||||
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()
|
||||
}
|
||||
})
|
||||
this.$socket.on("message", (message) => {
|
||||
if (message.chatId === this.chat.chatId) {
|
||||
this.messages.push(message)
|
||||
this.autoScroll()
|
||||
this.markRead()
|
||||
if (document.hasFocus()) {
|
||||
this.markRead()
|
||||
}
|
||||
if (this.messages.length > 50 && !this.avoidAutoScroll) {
|
||||
this.messages.shift()
|
||||
this.reachedTop = false
|
||||
|
@ -1265,6 +1429,11 @@ export default {
|
|||
})
|
||||
},
|
||||
watch: {
|
||||
"$store.state.context.pins.value"(val) {
|
||||
if (val) {
|
||||
this.getPins()
|
||||
}
|
||||
},
|
||||
userPanel() {
|
||||
localStorage.setItem("userPanel", JSON.stringify(this.userPanel))
|
||||
},
|
||||
|
@ -1287,6 +1456,7 @@ export default {
|
|||
this.reachedTop = false
|
||||
this.avoidAutoScroll = false
|
||||
this.offset = 0
|
||||
this.pins = []
|
||||
this.getMessages()
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue